Django测试开发学习笔记(四)-Django实战

214 阅读5分钟

参考github开源代码:github.com/githublitao…

环境搭建

新建项目

  • pacharm中选择django框架+虚拟环境

  • 安装django

    pip install django

数据库配置

  1. 修改settings.py

    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.mysql',
            'HOST': '127.0.0.1',  # 数据库主机
            'PORT': 3306,  # 数据库端口
            'USER': 'root',  # 数据库用户名
            'PASSWORD': 'admin1234',  # 数据库用户密码
            'NAME': 'api_test'  # 数据库名字
        }
    }
    
  2. 安装相关库:

    pip install mysqlclient==1.4.6

    pip install wheel

中间件配置

  1. 前提:使用rest-framework框架

    • 安装 :

      pip install djangorestframework

    • 修改settings.py :

      INSTALLED_APPS添加'rest_framework'

  2. 格式化响应输出

    • 新建utils文件夹,新建custom_response_middleware.py

      class CustomResponseMiddleware:
          def __init__(self, get_response):
              self.get_response = get_response
               # 配置和初始化
      
          def __call__(self, request):
      
              # 在这里编写视图和后面的中间件被调用之前需要执行的代码
              # 这里其实就是旧的process_request()方法的代码
              response = self.get_response(request)
              # if "code" not in response.data:
              #
              #     data = response.data
              #     response.data={
              #         "code":"0000",
              #         "message":"查询成功",
              #         "data":response.data
              #     }
              #     # 因返回时已经render过response,要想让这里的修改有效,需要手动在render一次
              # response._is_rendered = False
              # response.render()
              # response["content-length"]=len(response.content)
              # 在这里编写视图调用后需要执行的代码
              # 这里其实就是旧的 process_response()方法的代码
              return response
      
          def process_template_response(self, request, response):# 推荐
          
              if request.method == 'DELETE' and response.data is None:
                  response.data = {
                      "code": "0000",
                      "message": "删除成功",
                      "data": response.data
                      }
              if "code" not in response.data:
                  data = response.data
                  response.data={
                      "code":"0000",
                      "message":"操作成功",
                      "data":response.data
                  }
              # 在这里编写视图调用后需要执行的代码
              # 这里其实就是旧的 process_response()方法的代码
      
              return response
      
    • 修改settings.py :

      MIDDLEWARE中最前面增加'utils.custom_response_middleware.CustomResponseMiddleware'。(响应中间件放最前面,请求中间件放最后面)

重写异常类

utils/custom_exception.py

from rest_framework import status
from rest_framework.exceptions import ValidationError
from rest_framework.views import exception_handler as drf_exception_handler
from utils.custom_response import CustomResponse


def exception_handler(exc,context):
    """
    自定义异常处理
    :param exc: 别的地方抛的异常就会传给exc
    :param context: 字典形式。抛出异常的上下文(即抛出异常的出处;即抛出异常的视图)
    :return: Response响应对象
    """
    response = drf_exception_handler(exc,context)
    if response is None:
        # drf 处理不了的异常
        print('%s - %s - %s' % (context['view'], context['request'].method, exc))
        return CustomResponse({'detail': '服务器错误'}, code=500,msg="服务器内部错误",status=status.HTTP_500_INTERNAL_SERVER_ERROR, exception=True)
    if isinstance(exc,ValidationError):
        message = ""
        data = response.data
        for key in data:
            message += ";".join(data[key])
        return CustomResponse(None,code="9999",msg=message)
    return response

修改settings.py

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER':'utils.custom_exception.exception_handler',
}

重写响应类

utils/custom_response.py

from rest_framework.response import Response


# 重写响应
class CustomResponse(Response):

    def __init__(self, *args, code='0000', msg="成功", **kwargs):
        # 格式化data
        data = {
            "code": code,
            "message": msg
        }
        if args is not None:
            data["data"] = args[0]
            kwargs["data"] = data
        elif "data" in kwargs:
            data["data"] = kwargs["data"]
            kwargs["data"] = data

        super().__init__(**kwargs)

实战

新建应用

  • python manage.py startapp guoya_api

  • 添加应用到settings.pyINSTALLED_APPS

路由分发

主路由:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('v01/', include('guoya_api.urls')), 
]

子路由:

urlpatterns = [

]

models

文件位置:guoya_api>models

  • 直接使用开源代码中的models,再进行修改: github.com/githublitao…

  • 数据迁移

    生成迁移脚本:

    python manage.py makemigrations

    执行迁移标本:

    python manage.py migrate

对模型创建序列化器

文件位置:guoya_api>serializers.py

直接使用开源代码中的serializers:

github.com/githublitao…

  • 对模型中的Project的序列化器进行修改

    因为这几个字段、相关表都没有数据,先注释掉。

创建视图

使用到过滤后端

settings.py中INSTALLED_APPS添加'django_filters'

实现新增项目、查询项目(操作多个项目)
import django_filters
from . import serializers
from . import models
from rest_framework import generics
from rest_framework.response import Response

# 选择:
# APIView 不支持搜索展示所有
# GenericAPIView 支持搜索展示所有,使用过滤后端
# ModelViewSet

# 操作多个项目
class Projects(generics.GenericAPIView):
    queryset = models.Project.objects.all()
    serializer_class = serializers.ProjectSerializer
    # 针对某个GenericAPIView视图添加过滤后端
    filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
    # 设置查询关键字,一对一和一对多 可以使用__来指定具体的属性
    filterset_fields = ['name']

    def get(self, request, *args, **kwargs):
        """
        获取项目列表
        """

        # 使用filter_queryset方法对查询集进行过滤
        projects = self.filter_queryset(self.get_queryset())
        serializer = self.get_serializer(instance=projects, many=True)
        return Response(serializer.data)

    def post(self, request, *args, **kwargs):
        """
        新增项目
        """
        # 拿到前端传入数据
        data = request.data
        serializer = serializers.ProjectDeserializer(data=data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.validated_data)
  • 效果

    post请求:新增项目

    get请求:获取项目,支持按项目名精确搜索(如要实现模糊查询,参考前面讲过滤后端时的自定义查询类)

实现查询、修改、删除单个项目

restframework中对单个查、删、改时,如:

  • get请求 127.0.0.1:8000/projects/1 表示查询id为1的数据

  • put请求 127.0.0.1:8000/projects/1 表示修改id为1的数据

  • del请求 127.0.0.1:8000/projects/1 表示删除id为1的数据

因为查询和修改、删除使用的不是一个序列化器,所以要分开使用两个不同的类

查询单个使用:GenericAPIView+mixins:RetireveAPIView

修改、删除单个使用:GenericAPIView+mixins:UpdateAPIView、DestoryAPIView

views.py

# 操作单个项目
class Project(mixins.DestroyModelMixin,mixins.UpdateModelMixin,generics.GenericAPIView):
    queryset = models.Project.objects.all()
    serializer_class = serializers.ProjectDeserializer

    def get(self,request,*args, **kwargs):
        """
        查询单个项目信息
        """
        # 默认前端定义参数是pk时 ,相当于models.Project.objects.get(pk=pk)
        project = self.get_object()
        return Response(serializers.ProjectSerializer(instance=project).data)

    def put(self,request,*args, **kwargs):
        """
        修改单个项目
        """
        return self.update(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        """
        删除单个项目
        """
        return self.destroy(request, *args, **kwargs)

urls.py

urlpatterns = [
    re_path(r"projects/?", views.Projects.as_view()),
    re_path(r"^project/(?P<pk>[\d]+)/?$", views.Project.as_view()),
]
  • 效果

    查询单个项目:

    修改单个项目:

    删除单个项目:

实现存储测试用例相关信息的功能
  • 逻辑

    将请求需要的参数存储,执行测试用例时,去读取数据库数据

  • 涉及的表

    automationcaseapi 存储一条用例的详细信息,如用例名、请求方法、请求路径、请求参数、断言等。

    automationhead 存储用例对应的请求头信息

    automationparameter 存储键值对形式请求参数

    automationparameterraw 存储源格式的请求参数

  • 找到对应表的序列化和反序列化器

    AutomationCaseApiSerializer

    AutomationCaseApiDeserializer

    AutomationHeadSerializer

    AutomationHeadDeserializer

    AutomationParameterSerializer

    AutomationParameterDeserializer

    AutomationParameterRawSerializer

    AutomationParameterRawDeserializer

  • 基础数据准备

    因为实战时跳过了一些步骤,需要补充一些基础数据:

    globalhost表 - 测试域名

    automationgrouplevelfirst表 - 接口分组

    automationgrouptestcase表 - 测试套件表

  • 使用源数据执行测试用例时的传入的参数如下

    {
        "project_id": 1,
        "automationTestCase_id": 2,
        "name": "测试第一条用例",
        "httpType": "HTTP",
        "requestType": "POST",
        "apiAddress": "/login",
        "headDict": [{
    
                "name": "Content-Type",
                "value": "application/json",
                "interrelate": false
            },
            {
    
                "name": "",
                "value": "",
                "interrelate": false
            }
        ],
        "requestParameterType": "raw",
        "formatRaw": false,
        "requestList": "{\n \"pwd\":\"gy1234\",\n \"username\":\"leitx1234\"\n}",
        "examineType": "no_check",
        "RegularParam": "",
        "httpCode": "",
        "responseData": ""
    }
    
  • 使用键值对形式传入时,请求参数如下: 不同处:requestParameterTyperequestList

{ "project_id": 1, "automationTestCase_id": 2, "name": "测试第二条用例", "httpType": "HTTP", "requestType": "POST", "apiAddress": "/login", "headDict": [{

		"name": "Content-Type",
		"value": "application/json",
		"interrelate": false
	},
	{

		"name": "",
		"value": "",
		"interrelate": false
	}
],
"requestParameterType": "form-data",
"formatRaw": false,
"requestList": [{"name":"username","value":"leitx1234","interrelate":false},{"name":"pwd","value":"gy1234","interrelate":false}],
"examineType": "no_check",
"RegularParam": "",
"httpCode": "",
"responseData": ""
}
- 视图

  views.py
```py
class TestCases(APIView):
  def post(self,request):
      # 获取所有请求参数

      data = request.data
      print(data)
      # 剔除掉不需要存入数据库的数据
      headers = data.pop("headDict")
      body_data = data.pop("requestList")
      project_id = data.pop("project_id")
      regular_param = data.pop("RegularParam")
      # 反序列化时,涉及到外键时,先把外键对象拿到,再把外键对象通过save方法传入
      obj = models.AutomationTestCase.objects.get(id=data.pop("automationTestCase_id"))
      # 把接口详细信息存入automationcaseapi这个表中
      serializer = serializers.AutomationCaseApiDeserializer(data=data)
      serializer.is_valid(raise_exception=True)
      # 把外键对象通过save方法传入
      test_case = serializer.save(automationTestCase=obj)

      for h in headers:
          if h['name'] == "":
              continue
          serializer = serializers.AutomationHeadDeserializer(data=h)
          serializer.is_valid(raise_exception=True)
          serializer.save(automationCaseApi=test_case)

      if data["requestParameterType"] == 'raw':
          raw ={"data":body_data}
          serializer= serializers.AutomationParameterRawDeserializer(data=raw)
          serializer.is_valid(raise_exception=True)
          serializer.save(automationCaseApi=test_case)

      elif (data["requestParameterType"] =='form-data'):
          for p in body_data:
              serializer = serializers.AutomationParameterDeserializer(data=p)
              serializer.is_valid(raise_exception=True)
              serializer.save(automationCaseApi=test_case)
          pass

      return Response("ok")

  • 路由

    urlpatterns = [
        re_path(r"projects/?", views.Projects.as_view()),
        re_path(r"^project/(?P<pk>[\d]+)/?$", views.Project.as_view()),
        re_path(r"cases/?", views.TestCases.as_view()),
    
    ]
    
  • 效果

    源格式

    源格式存储

    键值对

    键值对形式存储

实现执行测试用例的功能
  • 定报文

    至少需要知道api_id、host_id,知道host_id指向测试的环境,知道api_id就能知道测试用例的相关信息

    {
    	"api_id":5,
        "host_id":1 
    	}
    
  • 视图(似乎有点问题)

    class ExcuteCase(APIView):
    
        # 选择视图:执行的逻辑需要自定义排除ModelViewSet,对查询结果不进行筛选不需要分页排除GenericAPIView
        # 需要自定义报文内容,综上,选择APIView
        def post(self, request):
            # 获取请求数据,暂时不实现对请求数据的校验
            data = request.data
            # 获取要执行的接口id
            api_id = data["api_id"]
            # 获取主机id
            host_id = data["host_id"]
            # 暂时不实现校验接口数据是否存在,数据是否有缺失
            # 拿到api_id、host_id后,需要通过这两个数据运行用例,视图主要写校验代码,业务逻辑代码写在common文件夹下。
    
            # 直接调用业务逻辑的方法
            run_api(host_id=host_id, api_id=api_id)
            case_api = models.AutomationCaseApi.objects.filter(id=api_id).first()
            # 最新的结果在最后一条
            res = case_api.test_result.all().last()
            serializer = serializers.AutomationTestResultSerializer(instance=res)
            return Response(serializer.data)
    
  • 业务逻辑部分,写在common文件夹下automation_case.py

    import json
    import requests
    
    from guoya_api import models, serializers
    
    def run_api(host_id, api_id):
        # 从数据库获取host
        host = models.GlobalHost.objects.filter(id=host_id).first().host
        # 获取接口编号为api_id的用例对象
        case_api = models.AutomationCaseApi.objects.filter(id=api_id).first()
        data = serializers.AutomationCaseApiSerializer(instance=case_api).data
    
        # 获取请求方法
        # request_method = data['requestType'].upper() # 另一种写法
        request_method = case_api.requestType.upper()  # 另一种写法
    
        # 拼接url 协议名://ip:port/请求地址
        url = data['httpType'].lower() + "://" + host + data["apiAddress"]
        print("url:{}".format(url))
        # 获取请求头
        header_objs = case_api.header.all()
        headers = {h.name: h.value for h in header_objs}
    
        # 获取请求数据
        response = None
        param = None
        json_data = None
        if data["requestParameterType"] == "form-data":
            api_query_set = case_api.parameterList.all()
    
            param = {q.name: q.value for q in api_query_set}
            response = send_request(method=request_method,url=url,param=param,headers=headers)
    
        elif data["requestParameterType"] == "raw":
            print("*****raw**这里{}".format(case_api.parameterRaw))
            api_body = case_api.parameterRaw
    
            # 转为字典格式
            json_data = json.loads(api_body.data)
            response = send_request(method=request_method, url=url, data=json_data, headers=headers)
    
        response_header = response.headers
        response_data = response.json()
        response_status_code = response.status_code
        # 把数据存入automationtestresult表中
        models.AutomationTestResult(automationCaseApi=case_api,url=url,requestType=request_method,host=host,
                                    header=json.dumps(headers),parameter="&".join(["{}={}".format(k,param[k])
                                    for k in param]) if param is not None else json.dumps(json_data),
                                    httpStatus=response_status_code,examineType=api_id.examineType,data=None,result="PASS",
                                    responseData=json.dumps(response_data)).save()
    
        return response_header,response_data,response_status_code
    
    def send_request(method=None,url=None,param=None,data=None,headers = None):
        res = None
        if method.upper()=="POST":
            res = requests.request(method=method,url=url,data=param,json=data,headers = headers)
        elif method.upper() == 'GET':
            res = requests.request(method=method, url=url, param=param,headers = headers)
        return res