本接口自动化框架采用 python + unittest + request + openpyxl + myddt + pymysql 来实现接口自动化。

1、总体框架

2、单元测试框架 unittest

unittest 是 Python 自带的一个单元测试框架

2.1 作用

  • 管理用例

  • 批量执行用例

  • 组织运行结果/报告

  • 让代码更稳健

  • 可拓展

2.2 unittest 框架中,有以下几个组件:

TestCase:即测试用例,Unittest提供testCase类来编写测试用例,一个TestCase的实例就是一个测试用例。一条测试用例就是一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown),通过运行一条测试用例,可以对某一个问题进行验证。

Fixture:即测试固件,用于测试用例环境的搭建和销毁。在测试步骤执行前需要为该测试用例准备环境(SetUp),如启动app或打开浏览器,测试步骤执行后需要恢复环境(TearDown),如关闭app或浏览器,这时候就需要用到Fixture,使代码更简洁。

TestSuite:即测试套件,把需要执行的测试用例集合在一起就是TestSuite。使用TestLoader来加载TestCase到TestSuite中

TextTestRunner:即测试执行器,用于执行测试用例。该模块中提供run方法执行TestSuite中的测试用例,并返回测试用例的执行结果,如运行的用例总数、用例通过数、用例失败数。

report:即测试报告。unittest框架没有自带的用于生成测试报告的模块或接口,需要使用第三方的扩展模块HTMLTestRunner。

2.3跳过执行测试用例共有四种写法

  • @unittest.skip(reason) :跳过测试用例,reason 为测试被跳过的原因。
  • @unittest.skipIf(condition, reason) :当 condition 为真时,跳过测试用例。
  • @unittest.skipUnless(condition, reason) :跳过测试用例,除非 condition 为真。

2.4 断言

2.5 报告

from BeautifulReport import BeautifulReportfrom common.HTMLTestRunnerNew importHTMLTestRunner# 4种测试报告"""1、生成 HTML 类型2、生成 Br 类型3、生成 txt 类型"""# ts0 = unittest.TestLoader().discover('test_cases')# with open('reports/html_do接口自动化.html','wb') as f:# runner = HTMLTestRunner(f)# runner.run(ts0)## ts1 = unittest.TestLoader().discover('test_cases')# br = BeautifulReport(ts1)# br.report(description='DO',filename='br_do接口自动化',report_dir='reports',theme='theme_memories')### ts2 = unittest.TestLoader().discover('test_cases')# with open('reports/txt_do接口自动化.txt','w+') as f:# unittest.TextTestRunner(f,2).run(ts2)if __name__ == "__main__":unittest.main()

3、基础框架搭建

在项目根目录下新建 common 文件夹,用来存储公用方法。

在项目根目录下新建 reports 文件夹,用来存储项目报告。

在项目根目录下新建 logs 文件夹,用来存储结果日志。

在项目根目录下新建 test_data 文件夹,用来存储用例数据。

在项目根目录下新建 test_cases 文件夹,用例存储测试用例模块。

在项目根目录下新建 main.py 文件,作为入口函数,方便项目调试。

3.1common公用方法文件

3.1.1 init.py

# /usr/bin/env python# __*__ coding: utf-8 __*__# @Time : 2021/9/9 22:22# @Author: 夜华import settingsfrom common.log_handler import get_loggerfrom common.db_handler import DB# 日志logger = get_logger(**settings.LOG_CONFIG)# 数据库db = DB(settings.DB_CONFIG)

3.1.2 http_requests.py

"""-*- coding: utf-8 -*-@Author : 夜华@Time : 2023/5/14 3:38 PM@File : cliend_http_requests.py@Project : PyCharm"""import requestsdef cliend_http_requests(url,method,**kwargs):method = method.lower()return getattr(requests,method)(url,**kwargs)if __name__ == "__main__":case = {'url' : 'http://10.21.5.74:33140/api/v1/login','method' : 'post','requests':{'json' : {"email": "name", "password": "password"},'headers' : {"Content-Type": "application/json;charset=UTF-8"}}}response = cliend_http_requests(url=case['url'],method=case['method'],**case['requests'])print(response.json())

3.1.3 data_handler.py

"""-*- coding: utf-8 -*-@Author : 夜华@Time : 2023/5/14 4:10 PM@File : data_handler.py@Project : PyCharm"""import jsonfrom openpyxl import load_workbookdef get_test_data(filename,sheet_name):wb = load_workbook(filename=filename)sh = wb[sheet_name]row = sh.max_rowcolumn = sh.max_columndata = []keys = []for i in range(1,column+1):keys.append(sh.cell(1,i).value)for i in range(2,row+1):temp = {}for j in range(1,column+1):temp[keys[j-1]] = sh.cell(i,j).valuetry:temp['request'] = json.loads(temp['request'])temp['exportx_code'] = json.loads(temp['exportx_code'])except json.decoder.JSONDecodeError:raise ValueError('json数据转换错误')data.append(temp)return dataif __name__ == "__main__":res = get_test_data(filename='../test_data/test_cases.xlsx',sheet_name='login')print(res[0])

3.1.4 db_handler.py

"""-*- coding: utf-8 -*-@Author : 夜华@Time : 2023/5/14 2:58 PM@File : db_config_handler.py@Project : PyCharm"""import settingsimport pymysqlclass DB:def __init__(self,db_config):self.conn = pymysql.connect(**db_config)def sql_one(self,sql):with self.conn.cursor() as cursor:cursor.execute(sql)return cursor.fetchone()def sql_many(self,sql,size=int):with self.conn.cursor() as cursor:cursor.execute(sql)return cursor.fetchmany(size)def sql_all(self,sql):with self.conn.cursor() as cursor:cursor.execute(sql)return cursor.fetchall()def exisx(self,sql):with self.conn.cursor() as cursor:cursor.execute(sql)if cursor.fetchone():return Trueelse:return Falsedef sql_update(self,sql):with self.conn.cursor() as cursor:try:cursor.execute(sql)self.conn.commit()except:self.conn.rollback()return cursor.fetchone()def __del__(self):self.conn.close()if __name__ == "__main__":db = DB(db_config=settings.DB_CONFIG)print(db.sql_one("select * from help_category;"))print(db.sql_many("select * from help_category;",2))print(db.sql_all("select name from help_category;"))print(db.exisx("select * from help_category where name = 'Contents';"))print(db.sql_update("update help_category set url='' where name = 'Contents';"))

3.1.5 fixtrue

"""-*- coding: utf-8 -*-@Author : 夜华@Time : 2023/5/14 8:27 PM@File : fixtrue.py@Project : PyCharm"""import requestsimport settingsfrom common import loggerdef login(email,password):data = {'email': email,'password': password}headers = {"Content-Type":"application/json;charset=UTF-8"}url =settings.PROJECT_URL + settings.INTERFACE['login']res = requests.post(url=url,json=data,headers=headers)if res.status_code == 200:logger.info('用户登录成功')return res.json()else:logger.warning('用户登录失败')if __name__ == "__main__":res = login(email='name',password='password')

3.1.6 logs_handler.py

"""-*- coding: utf-8 -*-@Author : 夜华@Time : 2023/5/14 3:28 PM@File : log_handler.py@Project : PyCharm"""import loggingdef get_logger(name,filename,debug=False,fmt=None,mode='w',encoding='utf-8'):logger = logging.getLogger(name=name)logger.setLevel(level=logging.DEBUG)if debug:file_level = logging.DEBUGconsole_level = logging.DEBUGelse:file_level = logging.WARNINGconsole_level = logging.INFOif fmt is None:#fmt = '%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d] - %(message)s'fmt = '%(asctime)s-[%(filename)s-->line:%(lineno)d]-%(levelname)s:%(message)s'format = logging.Formatter(fmt)file_handler = logging.FileHandler(filename=filename,mode=mode,encoding=encoding)file_handler.setLevel(level=file_level)console_handler = logging.StreamHandler()console_handler.setLevel(level=console_level)file_handler.setFormatter(format)console_handler.setFormatter(format)logger.addHandler(file_handler)logger.addHandler(console_handler)return loggerif __name__ == "__main__":logger = get_logger(name='do',filename='../logs/do.txt',debug=False,mode='a')logger.debug(10)logger.info(20)logger.warning(30)logger.error(40)logger.critical(50)

3.1.7 reports_handler.py

"""-*- coding: utf-8 -*-@Author : 夜华@Time : 2023/5/14 3:42 PM@File : reports_handler.py@Project : PyCharm"""import osfrom BeautifulReport import BeautifulReportfrom common.HTMLTestRunnerNew import HTMLTestRunnerfromdatetime import datetimedef reports(ts,filename,report_dir,theme='theme_default',title=None,description=None,tester=None,_type='br'):time_prefix=datetime.now().strftime('%Y-%m-%d_%H:%M')filename = '{}_{}'.format(time_prefix,filename)if _type == 'br':br = BeautifulReport(ts)br.report(description=description,filename=filename,report_dir=report_dir,theme=theme)else:with open(os.path.join(report_dir,filename),'wb') as f:runner = HTMLTestRunner(f,title=title,description=description,tester=tester)runner.run(ts)

4、config 配置文件夹

4.1 config_dev.ini

[URL]api_url = http://10.21.5.74:33140

4.2 config_handler.py

# /usr/bin/env python# __*__ coding: utf-8 __*__# @Time : 2021/9/10 21:00# @Author: 夜华"""封装配置文件"""import yamlfrom configparser import ConfigParserdef get_config(filename,encoding='utf-8'):# 根据 . 获取文件后缀,并获取后面的内容suffix = filename.split('.')[-1]if suffix in ['ini','cfg','cng']: # 判断文件后缀是否存在列表内# 就是ini 配置config = ConfigParser() # 实例config.read(filename,encoding=encoding) # 读取文件data = {} #for section in config.sections(): #获取 文件里面的所有段名data[section] = dict(config.items(section))elif suffix in ['yaml','yml']:# 就是 yaml 配置with open(filename,'r',encoding=encoding) as f:data = yaml.load(f,Loader=yaml.FullLoader)else:raise ValueError('不能识别的配置后缀')return dataif __name__ == '__main__':get = get_config('../config.ini')print(get)res = get_config('../config.yaml')print(res)

4.3 init.py

"""-*- coding: utf-8 -*-@Author : 夜华@Time : 2023/6/1 2:18 PM@File : __init__.py@Project : PyCharm"""import osimport sysfrom config.config_handler import get_configBASE_DIR = os.path.dirname(os.path.abspath(__file__))if sys.argv[1] == "DEV":Config = get_config(os.path.join(BASE_DIR, './config_dev.ini'))else:Config = get_config(os.path.join(BASE_DIR, './config_test.ini'))

5、 logs 文件夹

保存接口测试过程中输出的日志

6、reports 文件夹

保存接口测试报告

7、test_cases 文件夹

7.1 base_case.py

"""-*- coding: utf-8 -*-@Author : 夜华@Time : 2023/5/14 4:39 PM@File : base_case.py@Project : PyCharm"""import unittestfrom common import logger,dbimport requestsimport settingsclass Basic_test_case(unittest.TestCase):name = '基类'logger = loggerrequests = requestssession = requests.session()db = dbsettings = settings@classmethoddef setUpClass(cls) -> None:cls.logger.info('---------------【{}】开始测试---------------'.format(cls.name))@classmethoddef tearDownClass(cls) -> None:cls.logger.info('---------------【{}】结束测试---------------'.format(cls.name))def check(self,case):self.logger.info('---------------【{}】开始测试---------------'.format(self.name))self.case = case # 测试用例self.step() # 测试步骤self.assert_status_code() # 断言状态码self.assert_json() # 断言响应信息self.assert_db() # 断言数据库是否存在数据self.logger.info('---------------【{}】结束测试---------------'.format(self.name))def step(self):self.case['url'] = self.settings.PROJECT_URL + self.settings.INTERFACE[self.case['url']]try:self.resposen = self.http_requests(url=self.case['url'],method=self.case['method'],**self.case['request'])except Exception as e:self.logger.warning('用例【{}】发送请求错误'.format(self.case['title']))self.logger.debug('url:【{}】'.format(self.case['url']))self.logger.debug('method:【{}】'.format(self.case['method']))raise eelse:self.logger.info('用例【{}】发送请求成功'.format(self.case['title']))def assert_status_code(self):try:self.assertEqual(self.resposen.status_code,self.case['status_code'])except AssertionError as e:self.logger.warning('用例【{}】状态码断言错误'.format(self.case['title']))self.logger.debug('预期状态码:【{}】'.format(self.case['status_code']))self.logger.debug('实际状态码:【{}】'.format(self.resposen.status_code))raise eelse:self.logger.info('用例【{}】状态码断言成功'.format(self.case['title']))def assert_json(self):res = self.resposen.json()res_data = {'phone':res.get('phone',None),'roleType':res.get('roleType',None)}try:self.assertEqual(res_data,self.case['exportx_code'])except AssertionError as e:self.logger.warning('用例【{}】响应信息断言错误'.format(self.case['title']))self.logger.debug('预期内容:【{}】'.format(self.case['exportx_code']))self.logger.debug('实际内容:【{}】'.format(res_data))self.logger.debug('响应内容:【{}】'.format(res))raise eelse:self.logger.info('用例【{}】响应信息断言成功'.format(self.case['title']))def assert_db(self):if self.case.get('sql'):try:db_res = self.db.exisx(self.case['sql'])self.assertTrue(db_res)except Exception as e:self.logger.warning('用例【{}】数据库查询失败'.format(self.case['title']))self.logger.debug('sql:【{}】'.format(self.case['sql']))raise eelse:self.logger.info('用例【{}】数据库查询成功'.format(self.case['title']))def http_requests(self,url,method,**kwargs)->requests.Response:method = method.lower()return getattr(self.session,method)(url=url,**kwargs)if __name__ == "__main__":unittest.main()

7.2 test_login.py

"""-*- coding: utf-8 -*-@Author : 夜华@Time : 2023/5/14 3:43 PM@File : test_login.py@Project : PyCharm"""import unittestimport settingsfrom test_cases.base_case import Basic_test_casefrom common.data_handler import get_test_datafrom common.myddt import data,ddtcases = get_test_data(settings.TEST_DATA,sheet_name='login')@ddtclass Test(Basic_test_case):name = '登录'@data(*cases)def test_01(self,case):self.check(case)if __name__ == "__main__":unittest.main()

8、test_data 文件夹

8.1 使用Excel表格维护测试用例

用例:id、title、url、method、requests、status_code、exportx_code、sql

9、main.py

main.py 为 测试入口。

from BeautifulReport import BeautifulReportfrom common.HTMLTestRunnerNew importHTMLTestRunnerimport settingsimport unittestfrom common.reports_handler import reportsts = unittest.TestLoader().discover('test_cases')runner = reports(ts,**settings.REPORTS_CONFIG)if __name__ == "__main__":unittest.main()

10、settings.py

"""-*- coding: utf-8 -*-@Author : 夜华@Time : 2023/5/14 2:53 PM@File : settings.py@Project : PyCharm"""import osimport project_apiBASE_DIR = os.path.dirname(os.path.abspath(__file__))TEST_DATA = os.path.join(BASE_DIR,'test_data/test_cases.xlsx')PROJECT_URL = 'http://10.00.5.74:00000'INTERFACE = {'login' : '/api/v1/login','query' : '/api/v1/approve/query" />

11、终端内执行

注意:DEV表示开发环境,如果想在非开发环境进行测试,就输入TEST。也可以在4.3init.py 修改