Python + request + unittest实现接口测试框架
1、为什么要写代码实现接口自动化
大家知道很多接口测试工具可以实现对接口的测试,如postman、jmeter、fiddler等等,而且使用方便,那么为什么还要写代码实现接口自动化呢?工具虽然方便,但也不足之处:
测试数据不可控制
接口测试本质是对数据的测试,调用接口,输入一些数据,随后,接口返回一些数据。验证接口返回数据的正确性。在用工具运行测试用例之前不得不手动向数据库中插入测试数据。这样我们的接口测试是不是就没有那么“自动化了”。
无法测试加密接口
这是接口测试工具的一大硬伤,如我们前面开发的接口用工具测试完全没有问题,但遇到需要对接口参 数进行加密/解密的接口,例如 md5、base64、AES 等常见加密方式。本书第十一章会对加密接口进行介绍。 又或者接口的参数需要使用时间戳,也是工具很难模拟的。
扩展能力不足
当我们在享受工具所带来的便利的同时,往往也会受制于工具所带来的局限。例如,我想将测试结果生 成 HMTL 格式测试报告,我想将测试报告发送到指定邮箱。我想对接口测试做定时任务。我想对接口测试做持续集成。这些需求都是工具难以实现的。
2、接口自动化测试设计
接口测试调用过程可以用下图概括,增加了测试数据库
一般的接口工具测试过程:
1、接口工具调用被测系统的接口(传参 username="zhangsan")。
2、系统接口根据传参(username="zhangsan")向正式数据库中查询数据。
3、将查询结果组装成一定格式的数据,并返回给被调用者。
4、人工或通过工具的断言功能检查接口测试的正确性。
接口自动化测试项目,为了使接口测试对数据变得可控,测试过程如下:
1、接口测试项目先向测试数据库中插入测试数据(zhangsan 的个人信息)。
2、调用被测系统接口(传参 username="zhangsan")。
3、系统接口根据传参(username="zhangsan")向测试数据库中进行查询并得到 zhangsan 个人信息。
4、将查询结果组装成一定格式的数据,并返回给被调用者。
5、通过单元测试框架断言接口返回的数据(zhangsan 的个人信息),并生成测试报告。
为了使正式数据库的数据不被污染,建议使用独立的测试数据库。
2、requests库
Requests 使用的是 urllib3,因此继承了它的所有特性。Requests 支持 HTTP 连接保持和连接池,支持使用cookie保持会话,支持文件上传,支持自动确定响应内容的编码。对request库的更详细的介绍可以看我之前接口测试基础的博客:
http://www.cnblogs.com/ailiailan/p/7388784.html
http://www.cnblogs.com/ailiailan/p/7412945.html
3、接口测试代码示例
下面以之前用python+django开发的用户签到系统为背景,展示接口测试的代码。
为什么开发接口?开发的接口主要给谁来用?
前端和后端分离是近年来 Web 应用开发的一个发展趋势。这种模式将带来以下优势:
1、后端可以不用必须精通前端技术(HTML/JavaScript/CSS),只专注于数据的处理,对外提供 API 接口。
2、前端的专业性越来越高,通过 API 接口获取数据,从而专注于页面的设计。
3、前后端分离增加接口的应用范围,开发的接口可以应用到 Web 页面上,也可以应用到移动 APP 上。
在这种开发模式下,接口测试工作就会变得尤为重要了。
开发实现的接口代码示例:
# 添加发布会接口实现 def add_event(request): eid = request.POST.get('eid','') # 发布会id name = request.POST.get('name','') # 发布会标题 limit = request.POST.get('limit','') # 限制人数 status = request.POST.get('status','') # 状态 address = request.POST.get('address','') # 地址 start_time = request.POST.get('start_time','') # 发布会时间 if eid =='' or name == '' or limit == '' or address == '' or start_time == '': return JsonResponse({'status':10021,'message':'parameter error'}) result = Event.objects.filter(id=eid) if result: return JsonResponse({'status':10022,'message':'event id already exists'}) result = Event.objects.filter(name=name) if result: return JsonResponse({'status':10023,'message':'event name already exists'}) if status == '': status = 1 try: Event.objects.create(id=eid,name=name,limit=limit,address=address,status=int(status),start_time=start_time) except ValidationError: error = 'start_time format error. It must be in YYYY-MM-DD HH:MM:SS format.' return JsonResponse({'status':10024,'message':error}) return JsonResponse({'status':200,'message':'add event success'})
通过POST请求接收发布会参数:发布会id、标题、人数、状态、地址和时间等参数。
首先,判断eid、name、limit、address、start_time等字段均不能为空,否则JsonResponse()返回相应的状态码和提示。JsonResponse()是一个非常有用的方法,它可以直接将字典转化成Json格式返回到客户端。
接下来,判断发布会id是否存在,以及发布会名称(name)是否存在;如果存在将返回相应的状态码和 提示信息。
再接下来,判断发布会状态是否为空,如果为空,将状态设置为1(True)。
最后,将数据插入到 Event 表,在插入的过程中如果日期格式错误,将抛出 ValidationError 异常,接收 该异常并返回相应的状态和提示,否则,插入成功,返回状态码200和“add event success”的提示。
# 发布会查询接口实现 def get_event_list(request): eid = request.GET.get("eid", "") # 发布会id name = request.GET.get("name", "") # 发布会名称 if eid == '' and name == '': return JsonResponse({'status':10021,'message':'parameter error'}) if eid != '': event = {} try: result = Event.objects.get(id=eid) except ObjectDoesNotExist: return JsonResponse({'status':10022, 'message':'query result is empty'}) else: event['eid'] = result.id event['name'] = result.name event['limit'] = result.limit event['status'] = result.status event['address'] = result.address event['start_time'] = result.start_time return JsonResponse({'status':200, 'message':'success', 'data':event}) if name != '': datas = [] results = Event.objects.filter(name__contains=name) if results: for r in results: event = {} event['eid'] = r.id event['name'] = r.name event['limit'] = r.limit event['status'] = r.status event['address'] = r.address event['start_time'] = r.start_time datas.append(event) return JsonResponse({'status':200, 'message':'success', 'data':datas}) else: return JsonResponse({'status':10022, 'message':'query result is empty'})
通过GET请求接收发布会id和name 参数。两个参数都是可选的。首先,判断当两个参数同时为空,接口返回状态码10021,参数错误。
如果发布会id不为空,优先通过id查询,因为id的唯一性,所以,查询结果只会有一条,将查询结果 以 key:value 对的方式存放到定义的event字典中,并将数据字典作为整个返回字典中data对应的值返回。
name查询为模糊查询,查询数据可能会有多条,返回的数据稍显复杂;首先将查询的每一条数据放到一 个字典event中,再把每一个字典再放到数组datas中,最后再将整个数组做为返回字典中data对应的值返回。
接口测试代码示例:
#查询发布会接口测试代码 import requests url = "http://127.0.0.1:8000/api/get_event_list/" r = requests.get(url, params={'}) result = r.json() print(result) assert result['status'] == 200 assert result['message'] == "success" assert result['data']['name'] == "xx 产品发布会" assert result['data']['address'] == "北京林匹克公园水立方" assert result['data']['start_time'] == "2016-10-15T18:00:00"
因为“发布会查询接口”是GET类型,所以,通过requests库的get()方法调用,第一个参数为调用接口的URL地址,params设置接口的参数,参数以字典形式组织。
json()方法可以将接口返回的json格式的数据转化为字典。
接下来就是通过 assert 语句对接字典中的数据进行断言。分别断言status、message 和data的相关数据等。
使用unittest单元测试框架开发接口测试用例
1 #发布会查询接口测试代码
2 import unittest
3 import requests class GetEventListTest(unittest.TestCase): def setUp(self): self.base_url = "http://127.0.0.1:8000/api/get_event_list/" def test_get_event_list_eid_null(self): ''' eid 参数为空 ''' r = requests.get(self.base_url, params={'eid':''}) result = r.json() self.assertEqual(result['status'], 10021) self.assertEqual(result['message'], 'parameter error') def test_get_event_list_eid_error(self): ''' eid=901 查询结果为空 ''' r = requests.get(self.base_url, params={'eid':901}) result = r.json() self.assertEqual(result['status'], 10022) self.assertEqual(result['message'], 'query result is empty') def test_get_event_list_eid_success(self): ''' 根据 eid 查询结果成功 ''' r = requests.get(self.base_url, params={'eid':1}) result = r.json() self.assertEqual(result['status'], 200) self.assertEqual(result['message'], 'success') self.assertEqual(result['data']['name'],u'mx6发布会') self.assertEqual(result['data']['address'],u'北京国家会议中心') def test_get_event_list_nam_result_null(self): ''' 关键字‘abc’查询 ''' r = requests.get(self.base_url, params={'name':'abc'}) result = r.json() self.assertEqual(result['status'], 10022) self.assertEqual(result['message'], 'query result is empty') def test_get_event_list_name_find(self): ''' 关键字‘发布会’模糊查询 ''' r = requests.get(self.base_url, params={'name':'发布会'}) result = r.json() self.assertEqual(result['status'], 200) self.assertEqual(result['message'], 'success') self.assertEqual(result['data'][0]['name'],u'mx6发布会') self.assertEqual(result['data'][0]['address'],u'北京国家会议中心')
48
49if __name__ == '__main__':
50 unittest.main()
unittest单元测试框架可以帮助组织和运行接口测试用例。
4、接口自动化测试框架实现
关于接口自动化测试,unittest 已经帮我们做了大部分工作,接下来只需要集成数据库操作,以及HTMLTestRunner测试报告生成扩展即可。
框架结构如下图:
pyrequests 框架:
db_fixture/: 初始化接口测试数据。
interface/: 用于编写接口自动化测试用例。
report/: 生成接口自动化测试报告。
db_config.ini : 数据库配置文件。
HTMLTestRunner.py unittest 单元测试框架扩展,生成 HTML 格式的测试报告。
run_tests.py : 执行所有接口测试用例。
4.1、数据库配置
首先,需要修改被测系统将数据库指向测试数据库。以 MySQL数据库为例,针对django项目而言,修改.../guest/settings.py 文件。可以在系统测试环境单独创建一个测试库。这样做的目的是让接口测试的数据不会清空或污染到功能测试库的数据。其他框架开发的项目与django项目类似,这个工作一般由开发同学完成,我们测试同学更多关注的是测试框架的代码。
4.2、框架代码实现
4.2.1、首先,创建数据库配置文件.../db_config.ini
4.2.2、接下来,简单封装数据库操作,数据库表数据的插入和清除,.../db_fixture/mysql_db.py
import pymysql.cursors import os import configparser as cparser # ======== Reading db_config.ini setting =========== base_dir = str(os.path.dirname(os.path.dirname(__file__))) base_dir = base_dir.replace('\\', '/') file_path = base_dir + "/db_config.ini" cf = cparser.ConfigParser() cf.read(file_path) host = cf.get("mysqlconf", "host") port = cf.get("mysqlconf", "port") db = cf.get("mysqlconf", "db_name") user = cf.get("mysqlconf", "user") password = cf.get("mysqlconf", "password") # ======== MySql base operating =================== class DB: def __init__(self): try: # Connect to the database self.connection = pymysql.connect(host=host, port=int(port), user=user, password=password, db=db, charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor) except pymysql.err.OperationalError as e: print("Mysql Error %d: %s" % (e.args[0], e.args[1])) # clear table data def clear(self, table_name): # real_sql = "truncate table " + table_name + ";" real_sql = "delete from " + table_name + ";" with self.connection.cursor() as cursor: cursor.execute("SET FOREIGN_KEY_CHECKS=0;") cursor.execute(real_sql) self.connection.commit() # insert sql statement def insert(self, table_name, table_data): for key in table_data: table_data[key] = "'"+str(table_data[key])+"'" key = ','.join(table_data.keys()) value = ','.join(table_data.values()) real_sql = "INSERT INTO " + table_name + " (" + key + ") VALUES (" + value + ")" #print(real_sql) with self.connection.cursor() as cursor: cursor.execute(real_sql) self.connection.commit() # close database def close(self): self.connection.close() # init data def init_data(self, datas): for table, data in datas.items(): self.clear(table) for d in data: self.insert(table, d) self.close() if __name__ == '__main__': db = DB() table_name = "sign_event" data = {'id':1,'name':'红米','`limit`':2000,'status':1,'address':'北京会展中心','start_time':'2016-08-20 00:25:42'} table_name2 = "sign_guest" data2 = {'realname':'alen','phone':12312341234,'email':'alen@mail.com','sign':0,'event_id':1} db.clear(table_name) db.insert(table_name, data) db.close()
首先,读取 db_config.ini 配置文件。 创建 DB 类,__init__()方法初始化,通过 pymysql.connect()连接数据库。
因为这里只用到数据库表的清除和插入,所以只创建 clear()和 insert()两个方法。其中,insert()方法对数 据的插入做了简单的格式转化,可将字典转化成 SQL 插入语句,这样格式转化了方便了数据库表数据的创建。
最后,通过 close()方法用于关闭数据库连接。
4.2.3、接下来接下来创建测试数据,.../db_fixture/test_data.py
import sys sys.path.append('../db_fixture') try: from mysql_db import DB except ImportError: from .mysql_db import DB # create data datas = { 'sign_event':[ {'id':1,'name':'红米Pro发布会','`limit`':2000,'status':1,'address':'北京会展中心','start_time':'2017-08-20 14:00:00'}, {'id':2,'name':'可参加人数为0','`limit`':0,'status':1,'address':'北京会展中心','start_time':'2017-08-20 14:00:00'}, {'id':3,'name':'当前状态为0关闭','`limit`':2000,'status':0,'address':'北京会展中心','start_time':'2017-08-20 14:00:00'}, {'id':4,'name':'发布会已结束','`limit`':2000,'status':1,'address':'北京会展中心','start_time':'2001-08-20 14:00:00'}, {'id':5,'name':'小米5发布会','`limit`':2000,'status':1,'address':'北京国家会议中心','start_time':'2017-08-20 14:00:00'}, ], 'sign_guest':[ {'id':1,'realname':'alen','phone':13511001100,'email':'alen@mail.com','sign':0,'event_id':1}, {'id':2,'realname':'has sign','phone':13511001101,'email':'sign@mail.com','sign':1,'event_id':1}, {'id':3,'realname':'tom','phone':13511001102,'email':'tom@mail.com','sign':0,'event_id':5}, ], } # Inster table datas def init_data(): DB().init_data(datas) if __name__ == '__main__': init_data()
init_data()函数用于读取 datas 字典中的数据,调用 DB 类中的 clear()方法清除数据库,然后,调用 insert() 方法插入表数据。
4.2.4、编写接口测试用例。创建添加发布会接口测试文件.../interface/add_event_test.py
import unittest import requests import os, sys parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, parentdir) from db_fixture import test_data class AddEventTest(unittest.TestCase): ''' 添加发布会 ''' def setUp(self): self.base_url = "http://127.0.0.1:8000/api/add_event/" def tearDown(self): print(self.result) def test_add_event_all_null(self): ''' 所有参数为空 ''' payload = {'eid':'','':'','limit':'','address':"",'start_time':''} r = requests.post(self.base_url, data=payload) self.result = r.json() self.assertEqual(self.result['status'], 10021) self.assertEqual(self.result['message'], 'parameter error') def test_add_event_eid_exist(self): ''' id已经存在 ''' payload = {'} r = requests.post(self.base_url, data=payload) self.result = r.json() self.assertEqual(self.result['status'], 10022) self.assertEqual(self.result['message'], 'event id already exists') def test_add_event_name_exist(self): ''' 名称已经存在 ''' payload = {'} r = requests.post(self.base_url,data=payload) self.result = r.json() self.assertEqual(self.result['status'], 10023) self.assertEqual(self.result['message'], 'event name already exists') def test_add_event_data_type_error(self): ''' 日期格式错误 ''' payload = {'} r = requests.post(self.base_url,data=payload) self.result = r.json() self.assertEqual(self.result['status'], 10024) self.assertIn('start_time format error.', self.result['message']) def test_add_event_success(self): ''' 添加成功 ''' payload = {'eid':11,'name':'一加4手机发布会','limit':2000,'address':"深圳宝体",'start_time':'2017-05-10 12:00:00'} r = requests.post(self.base_url,data=payload) self.result = r.json() self.assertEqual(self.result['status'], 200) self.assertEqual(self.result['message'], 'add event success') if __name__ == '__main__': test_data.init_data() # 初始化接口测试数据 unittest.main()
在测试接口之前,调用test_data.py文件中的init_data()方法初始化数据库中的测试数据。
创建AddEventTest测试类继承 unittest.TestCase 类,通过创建测试用例,调用相关接口,并验证接口返回 的数据。
4.2.5、创建run_tests.py文件
当开发的接口达到一定数量后,就需要考虑分文件分目录的来划分接口测试用例,如何批量的执行不同文件目录下的用例呢?unittest单元测试框架提供的discover()方法可以帮助我们做到这一点。并使用 HTMLTestRunner 扩展生成 HTML 格式的测试报告。
import time, sys sys.path.append('./interface') sys.path.append('./db_fixture') from HTMLTestRunner import HTMLTestRunner import unittest from db_fixture import test_data # 指定测试用例为当前文件夹下的 interface 目录 test_dir = './interface' discover = unittest.defaultTestLoader.discover(test_dir, pattern='*_test.py') if __name__ == "__main__": test_data.init_data() # 初始化接口测试数据 now = time.strftime("%Y-%m-%d %H_%M_%S") filename = './report/' + now + '_result.html' fp = open(filename, 'wb') runner = HTMLTestRunner(stream=fp, title='Guest Manage System Interface Test Report', description='Implementation Example with: ') runner.run(discover) fp.close()
首先,通过调用test_data.py文件中的init_data()函数来初始化接口测试数据。
使用unittest框架所提供的discover()方法,查找 interface/ 目录下,所有匹配*_test.py 的测试文件(*星 号匹配任意字符)。
HTMLTestRunner 为unittest单元测试框架的扩展,利用它所提供的HTMLTestRunner()类来替换unittest单元测试框架的TextTestRunner()类,从而生成HTML格式的测试报告。
遗憾的是HTMLTestRunner并不支持Python3.x,大家可以在网上找到适用于Python3.x的HTMLTestRunner.py文件,使用在自己的接口自动化工程中。
通过 time 的 strftime()方法获取当前时间,并且转化成一定的时间格式。作为测试报告的名称。这样做目的是是为了避免因为生成的报告的名称重名而造成报告的覆盖。最终,将测试报告存放于report/目录下面。如下图,一张完整的接口自动化测试报告。
Python + request + unittest实现接口测试框架的更多相关文章
- Python+selenium+unittest+HTMLTestReportCN单元测试框架分享
分享一个比较基础的,系统性的知识点.Python+selenium+unittest+HTMLTestReportCN单元测试框架分享 Unittest简介 unittest是Python语言的单元测 ...
- python用unittest+HTMLTestRunner的框架测试并生成测试报告
直接贴代码: import unittestfrom selenium import webdriverfrom time import sleepimport osimport time # 定义打 ...
- python+requests+unittest API接口测试
黑熊再网上查找了下接口测试相关的资料,大都重点是以数据驱动的形式,见用例维护在文本或表格中,而没有说明怎么样去生成想要的用例, 问题: 测试接口时,比如参数a,b,c,我要先测a参数,有(不传,为空, ...
- python的unittest測试框架的扩展浅谈
非常多时候測试框架须要依据測试数据来自己主动生成測试用例脚本,比方接口測试,通过不同參数构建组合去请求接口,然后验证返回结果.假设这样能通过配置excel数据来驱动測试.而不用去写一大堆的測试用例脚本 ...
- python做一个http接口测试框架
目录结构 project case#测试用例 suite#测试目录 logs#测试日志 papi#测试类 result#测试结果 setting.py#配置文件 1.日志类,用于测试时日志记录 pya ...
- Python request库与爬虫框架
Requests库的7个主要方法 requests.request():构造一个请求,支持以下各方法的基础方法 requests.get():获取HTML网页的主要方法,对应于HTTP的GET ...
- 【转】python做一个http接口测试框架
出处: https://my.oschina.net/bysu/blog/751634 https://my.oschina.net/u/3041656/blog/820023
- python+request+unittest+HTMLTestRunner
https://www.imooc.com/article/details/id/20813 https://www.cnblogs.com/fennudexiaoniao/p/7771931.htm ...
- 初探接口测试框架--python系列6
点击标题下「蓝色微信名」可快速关注 坚持的是分享,搬运的是知识,图的是大家的进步,没有收费的培训,没有虚度的吹水,喜欢就关注.转发(免费帮助更多伙伴)等来交流,想了解的知识请留言,给你带来更多价值,是 ...
随机推荐
- JavaScript 知识点
JS基础 页面由三部分组成: html:超文本标记语言,负责页面结构 css:层叠样式表,负责页面样式 js:轻量级的脚本语言,负责页面的动效和数据交互 小总结:结构,样式和行为,三者相分离 在htm ...
- 解决react-router4在browserhistory路由下的nginx的白屏或者404问题
使用react-router,官方推荐用browserhistory,美观简洁.但是nginx服务器端的配置也让人头疼. 首先看官方举例的方法: server { location / { try_f ...
- Ubantu16.04 redis安装
通过FTP方式将redis的安装包从windows上传到linux上 解压命令:$sudo tar -zxf ~/Downloads/redis-3.2.7.tar.gz -C /usr/local ...
- 【JavaWeb】图书管理系统【总结】
感想 该项目是目前为止,我写过代码量最多的项目了.....虽然清楚是没有含金量的[跟着视频来写的],但感觉自己也在进步中...... 写的过程中,出了不少的问题.....非常多的Servlet,JSP ...
- Hibernate学习(一)创建数据表
(1)生成数据库表的创建: // 默认读取hibernate.cfg.xml文件 Configuration cfg = new Configuration().configure(); // 生成并 ...
- 发布iOS应用时,Xcode报错:Application failed codesign verification.
如下图,在发布应用时,因为codesign问题卡住了.尝试修改Target中的code sign setting,没有效果. 接着,在Developer Center删除所有证书,甚至包括Apps I ...
- uva225 回溯剪枝
这道题要剪枝,不然会超时,还有就是每次参加过的城市下次不能再参观,不然会WA. 对于障碍物的坐标我用了两种方法,第一种就是直接用STL里面的set,对于判断是否访问过直接用的count,用时960ms ...
- Kubernetes 使用私服镜像
非常感谢这些无私知识分享的同僚 重要参考:http://blog.csdn.net/u013812710/article/details/52766227 1.首先你得有个私库,这里我用的是阿里云私库 ...
- mysql字符设置
MySQL字符集设置 mysql>CREATE DATABASE IF NOT EXISTS mydb default charset utf8 COLLATE utf8_general_ci; ...
- react 父子组件互相通信
1,父组件向子组件传递 在引用子组件的时候传递,相当于一个属性,例如: class Parent extends Component{ state = { msg: 'start' }; render ...