一、接口设计
接口名称: /test_steps/run/
请求方式: POST
参数格式: JSON
请求参数:
参数 | 变量名 | 类型 | 说明 | 是否必传 |
测试环境id | env | 整数 | 环境id | 是 |
测试数据 | data | json | 测试步骤的详细数据 | 是 |
请求示例:json格式参数
{
"data": {
"id": 2,
"interface": {
"url": "/users/login/",
"method": "POST" },
"title": "登录失败",
"headers": {},
"request": {
"json": {
"username": "xinlan",
"password": "123123"
},
"params": {}},
"file": [],
"setup_script": "# 前置脚本(python):\n# global_tools:全局工具函数\n# data:用例数据 \n# env: 局部环境\n# ENV: 全局环境\n# db: 数据库操作对象\n",
"teardown_script": "# 后置脚本(python):\n# global_tools:全局工具函数\n# data:用例数据 \n# response:响应对象response \n# env: 局部环境\n# ENV: 全局环境\n# db: 数据库操作对象\n"
},
"env": 1
}
返回示例
响应状态码:
200 响应数据:
{
"name":"登录失败",
"log_data":[["INFO","【INFO】 | 开始执行用例:【登录失败】\n"],
["DEBUG","临时变量:\n{}"],["DEBUG","全局变量:\n{'host': 'http://127.0.0.1:8080','headers': {'customer-header': 'wahaha'}, 'key1': 'value1', 'key2': 'value2'}"],
["INFO","【INFO】 | *********执行前置脚本*********"],["INFO","【INFO】 | 发送 [POST]请求 : 请求地址为http://127.0.0.1:8080/users/login/:"],
["DEBUG","请求头:\n{'User-Agent': 'python-requests/2.28.1', 'Accept-Encoding': 'gzip, deflate','Accept': '*/*', 'Connection': 'keep-alive', 'customer-header': 'wahaha', 'Content-Length': '44', 'Content-Type': 'application/json'}"],
["DEBUG","请求体:\n{\n \"username\": \"xinlan\",\n \"password\": \"123123\"\n}"],
["INFO","【INFO】 | 请求响应状态码:401"],
["DEBUG","响应头:\n{'Date': 'Thu, 21 Jul 2022 12:42:41 GMT', 'Server': 'WSGIServer/0.2 CPython/3.8.5', 'Content-Type': 'application/json', 'WWW-Authenticate': 'Bearer realm=\"api\"', 'Vary': 'Accept, Origin', 'Allow': 'POST, OPTIONS', 'X-Frame-Options': 'DENY', 'Content-Length': '37', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'same-origin'}"],
["DEBUG","响应体:\n{\n \"detail\": \"用户名密码错误!\"\n}"],
["INFO","【INFO】 |*********执行后置脚本*********"],
["INFO","【INFO】 | 登录失败执行——>【通过】\n"]],
"url":"http://127.0.0.1:8080/users/login/",
"method":"POST",
"status_cede":401,
"response_header":{"Date":"Thu, 21 Jul 2022 12:42:41 GMT","Server":"WSGIServer/0.2 CPython/3.8.5","Content- Type":"application/json","WWW-Authenticate":"Bearer realm=\"api\"","Vary":"Accept, Origin","Allow":"POST, OPTIONS","X-Frame- Options":"DENY","Content-Length":"37","X-Content-Type- Options":"nosniff","Referrer-Policy":"same-origin"},"requests_header":{"User- Agent":"python-requests/2.28.1","Accept-Encoding":"gzip, deflate","Accept":"*/*","Connection":"keep-alive","customer-
header":"wahaha","Content-Length":"44","Content- Type":"application/json"},
"response_body":"{\n \"detail\": \"用户名密码错误!\"\n}",
"requests_body":"{\n \"username\": \"xinlan\",\n \"password\":\"123123\"\n}",
"state":"成功",
"run_time":"0.345s"
}
二、 后端代码
1. 测试执行函数
创建testplans/tasks.py 模块,然后编写如下代码:
# -*- coding: utf-8 -*-
# time: 2022/11/9 21:45
# file: tasks.py
# author: fade
from projects.models import TestEnv
from .models import TestScene, TestPlan
from .serializers import TestSceneRunSerializer, TestPlanRunSerializer
from reports.models import Record, Report
from celery import shared_task
from apitestengine.core.cases import run_test
def __get_env_config(env_id, debug=True):
"""获取测试环境的配置"""
env = TestEnv.objects.get(id=env_id)
variable = {**env.global_variable, **env.debug_global_variable} if debug else env.global_variable
_ENV = {
**variable, # 因为debug模式下,var中有临时的调试变量,解包不能写到下面,否自会覆盖真正的host,headers
'host': env.host,
'headers': env.headers,
}
config = {
'ENV': _ENV,
'DB': env.db,
'global_func': env.global_func
}
return config
def run_case(cases, env_id):
"""运行单条用例"""
# 准备好测试数据
# 环境
config = __get_env_config(env_id=env_id)
# 执行用例
res, debug_var = run_test(case_data=[{"Cases": [cases]}], env_config=config, debug=True)
# 获取执行的结果
result = res['results'][0]['cases'][0]
# 保存调试环境的下的环境变量
env = TestEnv.objects.get(id=env_id)
env.debug_global_variable = debug_var
env.save()
return result
def run_scene(pk, env_id):
""""执行测试场景"""
# 1.获取测试环境
config = __get_env_config(env_id, debug=True)
# 获取测试场景
scene = TestScene.objects.get(pk=pk)
# 根据测试场景对象组织测试场景的测试数据
cases = TestSceneRunSerializer(scene).data['scenedata_set']
# 套内用例排序
cases.sort(key=lambda x: x['sort'])
# 组装用例数据
test_case = {'Cases': [item['step'] for item in cases], 'name': scene.name}
res, debug_var = run_test(case_data=[test_case], env_config=config, debug=True)
env = TestEnv.objects.get(pk=env_id)
env.debug_global_variable = debug_var
env.save()
return res['results'][0]
@shared_task
def run_plan(plan_id, env_id, record_id):
"""执行测试计划"""
# 获取测试环境
config = __get_env_config(env_id=env_id, debug=False)
# 获取执行计划的数据
plan = TestPlan.objects.get(pk=plan_id)
task_data = TestPlanRunSerializer(plan).data
scene_list = []
for scene in task_data['scenes']:
cases = scene['scenedata_set']
cases.sort(key=lambda x: x['sort'])
scene_list.append({'Cases': [item['step'] for item in cases], 'name': scene['name']})
res = run_test(case_data=scene_list, env_config=config, debug=False)
# 保存运行结果
record = Record.objects.get(pk=record_id)
# 往执行结果中加字段
res['plan'] = plan_id
res['test_env'] = env_id
res['tester'] = record.tester
# 创建测试报告
Report.objects.create(info=res, record=record)
# 更新record中的数据
record.all = res.get('all', 0)
record.success = res.get('success', 0)
record.fail = res.get('fail', 0)
record.error = res.get('error', 0)
record.pass_rate = '{:.2f}'.format(100 * res.get('success', 0) / res.get('all', 0)) if res.get('all', 0) else "0"
record.status = '执行完毕'
record.save()
# return res['results']
其中,函数run_case 用来运行单条用例,_ get_env_config 函数用来获取需要运行的环境配置。
2. 视图
还是通过视图集action扩展,在测试步骤视图集中添加一个run 方法如下:
import os
from django.shortcuts import render
from django.conf import settings
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated
from rest_framework.exceptions import ValidationError
from rest_framework.decorators import action
from .filters import TestStepFilter
from .serializers import TestStepSerializer, NestInterfaceSerializer, UploadFileSerializer, TestPlanSerializer, \
TestSceneSerializer, TestSceneStepSerializer
from .models import TestStep, TestPlan, SceneData, UploadFile, TestScene
from projects.models import TestEnv
from .tasks import run_case, run_scene, run_plan
from reports.serializers import RecordSerializer
class TestStepViewSet(ModelViewSet):
"""测试步骤视图集"""
serializer_class = TestStepSerializer
queryset = TestStep.objects.all()
permission_classes = [IsAuthenticated]
# filterset_fields = ['interface']
filterset_class = TestStepFilter
def get_serializer_class(self):
"""
复写这个方法,实现不同得操作使用不同得序列化器
list,create,retrieve,update,partial_update,destroy
"""
if self.action == 'retrieve':
return NestInterfaceSerializer
return self.serializer_class
@action(methods=['post'], detail=False)
def run(self, request, *args, **kwargs):
# 获取测试数据
cases = request.data.get('data')
env_id = request.data.get('env')
if not env_id:
raise ValidationError('请求参数env必填')
try:
TestEnv.objects.get(id=env_id)
except:
raise ValidationError('参数env传入的值无效')
res = run_case(cases=cases, env_id=env_id)
return Response(res)
class UploadFileViewSet(ModelViewSet):
"""文件上传视图"""
queryset = UploadFile.objects.all()
serializer_class = UploadFileSerializer
permission_classes = [IsAuthenticated]
def perform_create(self, serializer):
# 限制文件大小,文件重复
# 生成info数据
size = self.request.data['file'].size
name = self.request.data['file'].name
if size > 1024 * 300:
raise ValidationError(detail='上传的文件大小不可超过300KB')
if os.path.isfile(settings.MEDIA_ROOT / name):
raise ValidationError(detail=f'文件【{name}】已存在')
file_type = self.request.data['file'].content_type
file_path = str(settings.MEDIA_ROOT / name)
info = [name, file_path, file_type]
serializer.save(info=info)
def perform_destroy(self, instance):
"""文件删除"""
# 删除本地保存的文件
os.remove(instance.file.path)
instance.delete()
class TestPlanViewSet(ModelViewSet):
"""测试计划视图"""
queryset = TestPlan.objects.all()
serializer_class = TestPlanSerializer
permission_classes = [IsAuthenticated]
filterset_fields = ['project']
@action(methods=['post'], detail=True)
def run(self, request, pk, *args, **kwargs):
# 获取环境id
env_id = request.data.get('env')
# 生成测试记录
serializer = RecordSerializer(data={
'test_env': env_id,
'plan': pk,
'status': '执行中',
'tester': request.user.username
})
# raise_exception这个参数为True,如果校验失败会直接抛出异常
serializer.is_valid(raise_exception=True)
record = serializer.save()
# 同步执行测试计划
run_plan(plan_id=pk,env_id=env_id,record_id=record.id)
return Response(serializer.data)
class TestSceneViewSet(ModelViewSet):
"""测试场景视图"""
queryset = TestScene.objects.all()
serializer_class = TestSceneSerializer
permission_classes = [IsAuthenticated]
filterset_fields = ['testplan', 'project']
@action(methods=['post'], detail=True)
def run(self, request, pk, *args, **kwargs):
# 获取环境id
env_id = request.data.get('env')
# 执行测试场景
res = run_scene(pk=pk, env_id=env_id)
return Response(res)
class TestSceneStepViewSet(ModelViewSet):
queryset = SceneData.objects.all().order_by('sort')
serializer_class = TestSceneStepSerializer
permission_classes = [IsAuthenticated]
filterset_fields = ['scene']
# order方法是实际的视图方法,写法跟普通的类试图中视图方法写法一致
# 装饰器action需要修饰整个额外的方法
# 其中参数mathods接受的是列表,表示要处理的http请求方法
# 额外的方法会自动生成路由,默认情况会使用方法的名称
# 例如:/test_scene_steps/order
# 1.当detail=False时会生成/test_scene_steps/order 表示要处理查询集
# 2.当detail=True时会生成/test_scene_steps/<int:pk>/order 表示要处理单个对象
# 自定义url路径,使用url_path参数,会代替方法名
# url_name参数是修改url默认的名字,默认的名字是小写模型名_方法名
@action(methods=['put'], detail=False, )
def order(self, request, *args, **kwargs):
"""排序接口"""
objs = []
for item in request.data:
obj = SceneData.objects.get(pk=item['id'])
obj.sort = item['sort']
obj.save() # 单独保存,推荐
# objs.append(obj)
# SceneData.objects.abulk_update(objs, ['sort']) # 批量保存
return Response(request.data)
3. 序列化器
# -*- coding: utf-8 -*-
# time: 2022/11/4 15:27
# file: serializers.py
# author: fade
from rest_framework import serializers
from projects.models import Interface
from projects.serializers import InterfaceSerializer
from .models import TestStep, TestPlan, SceneData, UploadFile, TestScene
class NestInterfaceSerializer(serializers.ModelSerializer):
"""嵌套测试步骤(接口)详情序列化器"""
# 可以用这个方法或者用depth,分离两个序列化器得原因是:不是每个操作都需要嵌套,当查看得时候才需要嵌套;
interface = InterfaceSerializer()
class Meta:
model = TestStep
fields = "__all__"
class TestStepSerializer(serializers.ModelSerializer):
"""接口用例/测试步骤序列化器"""
class Meta:
model = TestStep
fields = '__all__'
# 嵌套外键字段,这里使用嵌套得话,创建得时候就无法设置那个接口了,interface这个参数被设置为只读,所以要分情况去返回序列化器(见view);
# depth = 1
class UploadFileSerializer(serializers.ModelSerializer):
"""文件上传序列化器"""
class Meta:
model = UploadFile
fields = '__all__'
extra_kwargs = {'file': {'write_only': True}, 'info': {'read_only': True}}
class TestPlanSerializer(serializers.ModelSerializer):
"""测试计划"""
class Meta:
model = TestPlan
fields = '__all__'
class TestSceneSerializer(serializers.ModelSerializer):
"""测试场景序列化器"""
class Meta:
model = TestScene
fields = '__all__'
class NestTestStepSerializer(serializers.ModelSerializer):
"""嵌套测试步骤序列化器"""
class Meta:
model = TestStep
fields = ['id', 'title']
class TestSceneStepSerializer(serializers.ModelSerializer):
"""场景步骤序列化器"""
stepInfo = NestTestStepSerializer(source='step', read_only=True)
class Meta:
model = SceneData
fields = '__all__'
# class InterfaceRunSerializer(serializers.ModelSerializer):
# class Meta:
# model = Interface
# fields = '__all__'
class TestStepRunSerializer(serializers.ModelSerializer):
interface = InterfaceSerializer()
class Meta:
model = TestStep
fields = '__all__'
class TestSceneStepRunSerializer(serializers.ModelSerializer):
step = TestStepRunSerializer()
class Meta:
model = SceneData
fields = '__all__'
class TestSceneRunSerializer(serializers.ModelSerializer):
scenedata_set = TestSceneStepRunSerializer(many=True) # 因为是多条数据,所以设置many=True
class Meta:
model = TestScene
fields = '__all__'
class TestPlanRunSerializer(serializers.ModelSerializer):
scenes = TestSceneRunSerializer(many=True)
class Meta:
model = TestPlan
fields = '__all__'