接口自动化测试框架开发实践
再次整理接口自动化测试框架的内容,其实过往写了很多类似的文章,基本上从概念为大家介绍,现在计划用这篇文章手把手让框架从0到1建设起来。


一、基本概念
测试框架:诸如Unittest、Pytest、TestNG之类,它们的功能主要在于管理测试用例、执行测试用例、参数化、断言、生成测试报告等基础建设工作,像本文,就是在基于pytest的基础之上,进一步扩展接口自动化测试框架的其他内容
测试工具:类似selenium、jmeter、postman等,目的是为了完成某一类的测试工作,如UI自动化、接口测试等
二、被测系统
用Django写了一个简单计算器接口,主要功能为支持1-100的2个输入,并算出最后结果,代码如下所示:
# calculator_app/views.py from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt import json from django.shortcuts import render @csrf_exempt def calculate(request): if request.method == 'POST': try: data = json.loads(request.body) num1 = int(data['num1']) num2 = int(data['num2']) if 1 <= num1 <= 100 and 1 <= num2 <= 100: result = num1 + num2 return JsonResponse({'result': result}) else: return JsonResponse({'error': 'Numbers must be between 1 and 100'}, status=400) except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON'}, status=400) except ValueError: return JsonResponse({'error': 'Invalid input'}, status=400) else: return JsonResponse({'error': 'Invalid request method'}, status=405) def calculator_page(request): return render(request, 'calculator.html')
页面如下所示:

三、被测系统测试用例
| 用例编号 | 用例标题 | 输入1 | 输入2 | 预期 |
|---|---|---|---|---|
| TC001 | 正常情况测试(50+30) | 50 | 30 | 80 |
| TC002 | 边界值测试(两个最小值1+1) | 1 | 1 | 2 |
| TC003 | 边界值测试(两个最大值100+100) | 100 | 100 | 200 |
| TC004 | 非法输入测试(输入0) | 0 | 50 | error |
| TC005 | 非法输入测试(输入大于100的数) | 150 | 50 | error |
四、最简接口脚本实现
import requests import pytest def testcase1(): # 接口的URL url = 'http://192.168.8.220:8085/calculate/' # 请求头 headers = { 'Content-Type': 'application/json', } # 请求体 payload = {"num1": "50", "num2": "30"} # 发送POST请求 response = requests.post(url, headers=headers, json=payload) # 响应断言 # 检查HTTP状态码 response.raise_for_status() # 如果响应状态码不是200,将引发HTTPError异常 # 将响应内容解析为JSON json_response = response.json() # 具体的响应断言(根据你的接口文档来编写) assert json_response['result'] == 80 # 可以添加更多断言来验证响应内容 print("Test passed!") def testcase2(): # 接口的URL url = 'http://192.168.8.220:8085/calculate/' # 请求头 headers = { 'Content-Type': 'application/json', } # 请求体 payload = {"num1": "1", "num2": "2"} # 发送POST请求 response = requests.post(url, headers=headers, json=payload) # 响应断言 # 检查HTTP状态码 response.raise_for_status() # 如果响应状态码不是200,将引发HTTPError异常 # 将响应内容解析为JSON json_response = response.json() # 具体的响应断言(根据你的接口文档来编写) assert json_response['result'] == 3 # 可以添加更多断言来验证响应内容 print("Test passed!") if __name__ == '__main__': pytest.main(['-s',"example.py"]) 最简单的接口测试脚本如上所示,包含请求头、请求体、响应断言等,单看一条用例的话感觉没啥问题,但看后面的工作,多条用例实现的话,出现了大量的重复代码,而且数据和业务没有分离,如果需要调整,修改起来特别麻烦痛苦。
最简单的接口测试脚本如上所示,包含请求头、请求体、响应断言等,单看一条用例的话感觉没啥问题,但看后面的工作,多条用例实现的话,出现了大量的重复代码,而且数据和业务没有分离,如果需要调整,修改起来特别麻烦痛苦。
五、优化第一步,脚本参数化
testdata = [("50","30",80),("1","1",2),("100","100",200)] @pytest.mark.parametrize("data1,data2,result",testdata) def testcase(data1,data2,result): # 接口的URL url = 'http://192.168.8.220:8085/calculate/' # 请求头 headers = { 'Content-Type': 'application/json', } # 请求体 payload = {"num1": data1, "num2": data2} # 发送POST请求 response = requests.post(url, headers=headers, json=payload) # 响应断言 # 检查HTTP状态码 response.raise_for_status() # 如果响应状态码不是200,将引发HTTPError异常 # 将响应内容解析为JSON json_response = response.json() # 具体的响应断言(根据你的接口文档来编写) assert json_response['result'] == result # 可以添加更多断言来验证响应内容 print("Test passed!") if __name__ == '__main__': pytest.main(['-s',"example.py"]) 测试结果: ============================= test session starts ============================= platform win32 -- Python 3.7.3, pytest-7.2.1, pluggy-1.0.0 rootdir: F:\zhan\pythoncode\test_requests plugins: allure-pytest-2.13.1, Faker-18.6.2, base-url-2.0.0, html-3.2.0, metadata-2.0.4, playwright-0.3.3, xdist-3.2.1 collected 3 items example.py Test passed! .Test passed! .Test passed! .
通过@pytest.mark.parametrize这个装饰器的使用,使得不用再重复写多条一样的用例,把测试数据从用例里面剥离出阿里,从而达成了一个函数完成了多条用例的实现,这就是最简的数据驱动测试。
六、优化第二步,数据与脚本分离
通过上述优化之后,代码简化了不少,但是还存在一个问题,当不同业务多了之后,每次去调整数据的时候,还是需要进入代码里面去翻,现在最通用的办法是,让数据与脚本分离出来,创建一个新的data目录,让数据存储在data里面,如果出现了数据变动,直接去更改data目录里面的数据就行了。数据存储方面,有用excel、json、yaml的,目前来说,用yaml存储数据的比较常见,故用yaml作为示例:
加入读取yaml代码,读取用例名称、http请求、期望结果
import yaml def get_test_data(test_data_path): case = [] http = [] expected = [] with open(test_data_path, encoding='utf-8') as f: data = yaml.load(f.read(),Loader=yaml.SafeLoader) test = data['test'] for td in test: case.append(td.get('case','')) http.append(td.get('http','')) expected.append(td.get('expected','')) parameters = zip(case,http,expected) return case,parameters cases,parameters = get_test_data("data\\exampledata.yaml") print(cases,list(parameters))
创建data目录,里面创建yaml文件,把用例1的数据存储起来
test: - case: 验证成功的用例 http: method: POST path: http://192.168.8.220:8085/calculate/ headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 content-type: application/json;charset=UTF-8 params: num1: "30" num2: "50" expected: response: result: 80
然后再对测试用例进行改造:
cases,parameters = get_test_data("data\\exampledata.yaml") testdata = list(parameters) url = testdata[0][1]['path'] headers = testdata[0][1]['headers'] payload = testdata[0][1]['params'] result = testdata[0][2]['response']['result'] def testcase1(): response = requests.post(url, headers=headers, json=payload) response.raise_for_status() # 如果响应状态码不是200,将引发HTTPError异常 json_response = response.json() assert json_response['result'] == result # 可以添加更多断言来验证响应内容 print("Test passed!")
可以看到,测试用例精简了很多,再结合我们上面的数据驱动测试,再进行简单改造:
首先把所有的用例数据整理进入yaml文件:
test: - case: 验证成功的用例 http: method: POST path: http://192.168.8.220:8085/calculate/ headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 content-type: application/json;charset=UTF-8 params: num1: "30" num2: "50" expected: response: result: 80 - case: 验证边界值用例1 http: method: POST path: http://192.168.8.220:8085/calculate/ headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 content-type: application/json;charset=UTF-8 params: num1: "1" num2: "1" expected: response: result: 2 - case: 验证边界值用例2 http: method: POST path: http://192.168.8.220:8085/calculate/ headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 content-type: application/json;charset=UTF-8 params: num1: "100" num2: "100" expected: response: result: 200
然后再整改原有的数据驱动代码:
cases,parameters = get_test_data("data\\exampledata.yaml") yamldata = list(parameters) @pytest.mark.parametrize("case,http,expected",yamldata) def testcase1(case,http,expected): response = requests.post(http["path"], headers=http["headers"], json=http['params']) response.raise_for_status() # 如果响应状态码不是200,将引发HTTPError异常 json_response = response.json() assert json_response['result'] == expected["response"]['result'] # 可以添加更多断言来验证响应内容 print("Test passed!")
这样,我们就做到数据分离,也做到了测试用例的去重了,对比原先的流水化用例,是不是精简了很多
七、后续
经过上面整合后,我们的框架里面的数据部分已经分离出去了,但开始里面还有很多东西没完成,如配置、报告、日志等等,后续再更新。

浙公网安备 33010602011771号