自动化测试平台-用例执行
模板list_api.html:

{% extends 'base.html' %} {% load static %} {% block breadcrumb %} <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="{% url 'index' %}">Index</a></li> <li class="breadcrumb-item active">{{ it_obj.it_name }}</li> <li class="breadcrumb-item active">用例列表</li> </ol> {% endblock %} {% block content %} {# {% if it_obj %}#} {# <div class="card card-primary">#} {# <div class="card-header">#} {# <a href="{% url 'add_it' %}">添加项目</a>#} {# </div>#} <table class="table table-striped table-hover table-bordered"> <thead> <tr> <th>选择</th> <th>序号</th> <th>名称</th> <th>描述</th> <th>请求url</th> <th>请求类型</th> <th>请求参数</th> <th>预期值</th> <th>用例报告</th> <th>通过状态</th> <th>是否执行</th> <td>操作</td> </tr> </thead> <tbody id="chk1"> {% for foo in api_obj %} <tr> <td> <input type="checkbox" value="{{ foo.pk }}" name="checkbox_list"> </td> <td>{{ forloop.counter }}</td> <td>{{ foo.api_name }}</td> <td title="{{ foo.api_desc }}">{{ foo.api_desc | truncatechars:10 }}</td> <td title="{{ foo.api_url }}">{{ foo.api_url | truncatechars:10 }}</td> <td>{{ foo.api_method }}</td> <td>{{ foo.api_params }}</td> <td>{{ foo.api_expect }}</td> {% if foo.api_report %} <td><a href="{% url 'download_case_report' foo.pk %}">下载</a></td> {% else %} <td>无</td> {% endif %} <td>{{ foo.get_api_pass_status_display }}</td> <td>{{ foo.get_api_run_status_display }}</td> <td> <a href="{% url 'delete_api' foo.pk %}" class="btn btn-danger btn-sm">删除</a> <a href="{% url 'edit_api' foo.pk %}" class="btn btn-info btn-sm">编辑</a> <a href="{% url 'run_case' foo.pk %}" class="btn btn-info btn-sm">执行</a> </td> </tr> {% endfor %} </tbody> </table> <div> <input type="button" value="执行" class="btn btn-success" id="exeute"> </div> {# </div>#} {# {% else %}#} {# 没有数据,去 <a href="{% url 'add_it' %}">创建</a>#} {# {% endif %}#} {% endblock %} {% block js %} <script src="{% static 'sweetalert.min.js' %}"></script> <script> // 批量执行逻辑, /* 1. 点击执行按钮,获取到所有选中的CheckBox的值 - 点击后,没有找到选中的CheckBox,要给提示信息 2. 将多个值发送到后端 3. 后端处理完毕,返回前端状态 * */ $("#exeute").click(function () { var chk_value = new Array(); $.each($('input[name="checkbox_list"]:checked'), function (index, item) {//遍历,将所有选中的值放到数组中 console.log(item, index); chk_value.push($(item).val()); }); if (chk_value.length == 0) { swal({ "title": '没有勾选用例,请至少勾选一个用例后再点击执行!' }) } else { console.log(111111, chk_value); // ajax将 数组发送到后端 } }); </script> {% endblock %}
路由urls.py:

from django.contrib import admin from django.urls import path, re_path from app01 import views urlpatterns = [ path('admin/', admin.site.urls), path('', views.index, name="index"), path('index/', views.index, name="index"), path('add_it/', views.add_it, name='add_it'), re_path(r'^edit_it/(?P<pk>\d+)$', views.edit_it, name='edit_it'), re_path(r'^delete_it/(?P<pk>\d+)$', views.delete_it, name='delete_it'), # 接口用例表相关 re_path(r'^list_api/(?P<pk>\d+)$', views.list_api, name='list_api'), re_path(r'^add_api/(?P<pk>\d+)$', views.add_api, name='add_api'), re_path(r'^edit_api/(?P<pk>\d+)$', views.edit_api, name='edit_api'), re_path(r'^delete_api/(?P<pk>\d+)$', views.delete_api, name='delete_api'), # 执行用例 re_path(r'^run_case/(?P<pk>\d+)$', views.run_case, name='run_case'), # 下载用例执行报告 re_path(r'^download_case_report/(?P<pk>\d+)$', views.download_case_report, name='download_case_report'), ]
视图views.py:

from django.shortcuts import render, HttpResponse, redirect from django.http import JsonResponse from app01 import models from utils.MyModelForm import ItModelForm, ApiModelForm from utils import RequestHandler def index(request): """ 项目主页 """ if request.method == "POST": return JsonResponse({"code": 0, "message": "项目主页的post请求,非法"}) else: it_obj = models.It.objects.all() # print(1111111, it_obj) return render(request, 'index.html', {"it_obj": it_obj}) def add_it(request): """ 添加项目 """ if request.method == "POST": form_data = ItModelForm(request.POST) if form_data.is_valid(): form_data.save() return redirect('/index/') else: return render(request, 'add_it.html', {"it_form_obj": form_data}) else: it_form_obj = ItModelForm() return render(request, 'add_it.html', {"it_form_obj": it_form_obj}) def edit_it(request, pk): """ 编辑项目, pk:项目的pk """ it_obj = models.It.objects.filter(pk=pk).first() if request.method == "POST": form_data = ItModelForm(request.POST, instance=it_obj) if form_data.is_valid(): form_data.save() return redirect('/index/') else: return render(request, 'add_it.html', {"it_form_obj": form_data}) else: it_form_obj = ItModelForm(instance=it_obj) return render(request, 'edit_it.html', {"it_form_obj": it_form_obj}) def delete_it(request, pk): """ 删除项目表记录,pk:项目的pk """ models.It.objects.filter(pk=pk).delete() return redirect('/index/') def list_api(request, pk): """ 思考:要不要有 pk ? pk:项目的pk 查看某一个项目下的用例列表 """ api_obj = models.Api.objects.filter(api_sub_it_id=pk) it_obj = models.It.objects.filter(pk=pk).first() # print(1111111, it_obj.it_name) return render(request, 'list_api.html', {"api_obj": api_obj, 'it_obj': it_obj}) def add_api(request, pk): """ 添加用例, pk:所属项目的pk """ if request.method == "POST": form_data = ApiModelForm(request.POST) if form_data.is_valid(): form_data.instance.__dict__['api_sub_it_id'] = pk # form_data.instance.api_sub_it = it_obj form_data.save() return redirect('/index/') else: return render(request, 'add_api.html', {"api_form_obj": form_data}) else: api_form_obj = ApiModelForm() it_obj = models.It.objects.filter(pk=pk).first() return render(request, 'add_api.html', {"api_form_obj": api_form_obj, "it_obj": it_obj}) def edit_api(request, pk): """ 编辑用例, pk:api的pk """ api_obj = models.Api.objects.filter(pk=pk).first() if request.method == "POST": form_data = ApiModelForm(request.POST, instance=api_obj) if form_data.is_valid(): # print(111111111, form_data.instance.__dict__) form_data.instance.__dict__['api_pass_status'] = 0 form_data.instance.__dict__['api_run_status'] = 0 form_data.instance.__dict__['api_report'] = "" form_data.save() return redirect('/list_api/{}'.format(api_obj.api_sub_it_id)) # 用例列表接口需要所属项目的pk值 else: return render(request, 'edit_api.html', {"api_form_obj": form_data}) else: api_form_obj = ApiModelForm(instance=api_obj) return render(request, 'edit_api.html', {"api_form_obj": api_form_obj, "it_obj": api_obj.api_sub_it}) def delete_api(request, pk): """ 删除用例, pk:用例的pk """ # 由于返回时,需要项目的pk值,这里不能直接删除 api_obj = models.Api.objects.filter(pk=pk).first() # 获取所属项目的pk it_obj_pk = api_obj.api_sub_it_id api_obj.delete() return redirect('/list_api/{}'.format(it_obj_pk)) def run_case(request, pk=0): # ["11", "12"] """ 执行用例 """ case_obj = models.Api.objects.filter(pk=pk).first() # print(111111111, case_obj.api_data, type(case_obj.api_data)) RequestHandler.run_case(case_obj) it_obj_pk = case_obj.api_sub_it_id return redirect('/list_api/{}'.format(it_obj_pk)) from django.http import FileResponse from django.utils.encoding import escape_uri_path # 导入这个家伙 def download_case_report(request, pk): """ 下载用例的执行报告,pk:用例的pk """ api_obj = models.Api.objects.filter(pk=pk).first() # 下载 response = FileResponse(api_obj.api_report) response['Content-Type'] = 'application/octet-stream' response['Content-Disposition'] = 'attachment;filename="{}.{}"'.format(escape_uri_path(api_obj.api_name), 'html') return response
模型models.py:(添加logs)

from django.db import models # Create your models here. class It(models.Model): """接口项目表""" it_name = models.CharField(max_length=32, default="", verbose_name="项目名称") it_desc = models.TextField(max_length=255, default="", verbose_name="项目描述") it_start_time = models.DateField(verbose_name="项目开始时间") it_end_time = models.DateField(verbose_name="项目结束时间") def __str__(self): return self.it_name def xxoo(self): # 覆盖率 = 执行的用例数/总的用例数(总用例数不能为0) # result = self.api_set.count() / self.api_set.filter(api_pass_status=1).count() if self.api_set.count(): result = self.api_set.filter(api_pass_status=1).count() / self.api_set.count() # print(self.api_set.count(), self.api_set.filter(api_pass_status=1).count(), self.it_name, result) return result else: return "0.0" class Api(models.Model): """接口用例表""" api_sub_it = models.ForeignKey(to="It", verbose_name="所属接口项目", on_delete=models.SET_NULL, blank=True, null=True) api_name = models.CharField(max_length=32, default="", verbose_name="用例名称") api_desc = models.CharField(max_length=255, default="", verbose_name="用例描述") api_url = models.CharField(max_length=255, default="", verbose_name="请求URL") api_method = models.CharField(max_length=32, default='', verbose_name='请求类型') api_params = models.CharField(max_length=255, default={}, verbose_name='请求参数') api_data = models.CharField(max_length=255, default={}, verbose_name='请求data') api_expect = models.CharField(max_length=4196, default={}, verbose_name='预期结果') api_report = models.TextField(verbose_name="测试报告", default="") api_run_time = models.DateTimeField(null=True, verbose_name="执行时间") api_pass_status_choices = ( (0, "未通过"), (1, "已通过") ) api_pass_status = models.IntegerField(choices=api_pass_status_choices, verbose_name="执行是否通过", default=0) api_run_status_choices = ( (0, '未执行'), (1, '已执行'), ) api_run_status = models.IntegerField(choices=api_run_status_choices, verbose_name='是否执行', default=0) def __str__(self): return self.api_name class Logs(models.Model): """ 用例执行记录 1. 所属项目 2. 执行时间 3. 执行的测试报告 4. 通过多少,失败多少,共执行了多少用例,通过率是多少 """ log_report = models.TextField(verbose_name='报告', default='') log_sub_it = models.ForeignKey(to='It', verbose_name='所属接口项目', on_delete=models.CASCADE) log_run_time = models.DateTimeField(null=True, verbose_name='log日志产生时间', auto_now_add=True) log_pass_count = models.IntegerField(verbose_name='通过数量') log_failed_count = models.IntegerField(verbose_name='失败数量') log_errors_count = models.IntegerField(verbose_name='报错数量') log_run_count = models.IntegerField(verbose_name='执行用例总数') def pass_rate(self): """ 通过率 """ if self.log_run_count: return self.log_pass_count / self.log_run_count else: return 0
RequestHandler.py:

""" 处理请求相关和生成测试报告 """ import os import json import unittest import requests from deepdiff import DeepDiff from HTMLTestRunner import HTMLTestRunner from app01 import models from adminzdh.settings import BASE_DIR class MyCase(unittest.TestCase): def test_case(self): """ 用例 """ self._testMethodName = self.title self._testMethodDoc = self.desc self.assertEqual(DeepDiff(self.response, self.expect).get('type_changes', None), None, msg=self.msg) class RequestOperate(object): def __init__(self, case_obj): self.case_obj = case_obj def handler(self): """ 关于请求的一些列流程: 1. 提取case_obj中的字段,使用requests发请求 1. 对请求参数进行校验 2. 将请求结果提取出来 2. 使用unittest进行断言 3. 更新数据库字段 4. 将执行结果添加到日志表中 5. 前端返回 """ # 发请求,断言,并生成测试报告 self.send_msg() # 更新数据库字段 self.update_db_status() def send_msg(self): """ 发请求 """ response = requests.request( method=self.case_obj.api_method, url=self.case_obj.api_url, data=self._check_data(), params=self._check_params(), ) self.assert_msg(response) def assert_msg(self, response): """ 处理断言 DeepDiff""" case = MyCase(methodName='test_case') case.response = response.json() case.expect = self._check_expect() case.msg = "自定义的错误信息: <hr>{}".format(DeepDiff(response.json(), self._check_expect())) case.title = self.case_obj.api_name case.desc = self.case_obj.api_desc suite = unittest.TestSuite() suite.addTest(case) # unittest.TextTestRunner(verbosity=2).run(suite) self.get_report(suite) def get_report(self, suite): """ 生成用例报告 :param suite: 用例集 :return: """ f = open(os.path.join(BASE_DIR, 'a.html'), 'wb') # print(suite.countTestCases()) self.case_result = HTMLTestRunner( stream=f, verbosity=2, title=self.case_obj.api_name, description=self.case_obj.api_desc, ).run(suite) f.close() def update_db_status(self): """ 更新数据相关字段的状态 1. api_report 2. api_run_time 3. api_pass_status 4. api_run_status """ # obj = models.Api.objects.filter(pk=self.case_obj.pk).update( # api_report=self.read_file(), # ) # 写报告 obj = models.Api.objects.filter(pk=self.case_obj.pk).first() obj.api_report = self.read_file() # 写 执行时间 import datetime obj.api_run_time = datetime.datetime.now() # 写 api_run_status obj.api_run_status = 1 # 写 api_pass_status log_data = {'pass': 0, "failed": 0, "total": 0, "errors": 0} for i in self.case_result.__dict__['result']: if i[0]: # 用例执行失败 obj.api_pass_status = 0 log_data['failed'] += 1 else: obj.api_pass_status = 1 log_data['pass'] += 1 log_data['total'] += 1 log_data['errors'] = self.case_result.__dict__['errors'].__len__() obj.save() # 写log表,通过多少,失败多少,共执行了多少用例 models.Logs.objects.create( log_report=self.read_file(), log_sub_it_id=self.case_obj.api_sub_it_id, log_pass_count=log_data['pass'], log_errors_count=log_data['errors'], log_failed_count=log_data['failed'], log_run_count=log_data["total"] ) def read_file(self): """ 读文件""" with open(os.path.join(BASE_DIR, 'a.html'), 'r', encoding='utf-8') as f: return f.read() def _check_expect(self): """ 处理预期值 """ if self.case_obj.api_expect: return json.loads(self.case_obj.api_expect) else: return {} def _check_data(self): """ 校验请求的 data 参数 : 默认,数据库中的data字段是标准的json串 """ if self.case_obj.api_data: # print(2222222222, json.loads(self.case_obj.api_data)) return json.loads(self.case_obj.api_data) else: return {} def _check_params(self): """ 校验请求的 params 参数 : 默认,数据库中的 params 字段是标准的json串 """ if self.case_obj.api_params: return json.loads(self.case_obj.api_params) else: return {} def run_case(case_obj): # ["11", "12"] RequestOperate(case_obj=case_obj).handler()

<!-- 切片, 参数必须是 str --> <td>{{ foo.api_url | slice:"10"}}</td> <!-- 截取指定长度字符,后续以点代替, 参数必须是int --> <td title="{{ foo.api_url }}">{{ foo.api_url | truncatechars:10}}</td>
django的下载逻辑

from django.http import FileResponse
from django.http import StreamingHttpResponse
参考:https://www.cnblogs.com/Neeo/articles/11021972.html

Zhangkai's blogs朋友 资源 技术栈新建文章文章列表管理 Django - 文件上传下载 目录 about 上传 form表单上传 ajax上传文件 下载 使用StreamingHttpResponse 使用FileResponse 解决filename不能有中文的问题 结合media的文件下载 返回Django目录 返回随笔首页 about# 本篇博客主要演示了,使用Django完成对本地Excel表格的上传下载操作,当然,其他类型的文件也一样。 环境:win10 + django1.11 + xlrd 上传# 一般的, 上传可以分为通过form表单提交和通过ajax提交两种。 form表单上传# 来看示例: 前端重要代码。 <div> <form action="" method="post" enctype="multipart/form-data"> {% csrf_token %} <input type="file" name="f1"> <input type="text" name="user"> <input type="submit" value="提交"> </form> </div> 一般的,在普通的form表单提交时,请求头中的CONTENT_TYPE: application/x-www-form-urlencoded,然后数据是以键值对的形式传输的,服务端从request.POST取值,这没问题,并且CONTENT_TYPE: application/x-www-form-urlencoded这种编码类型满足大多数情况,一切都挺好的。 而要说使用form表单上传文件,就不得不多说两句了。 起初,http 协议中没有上传文件方面的功能,直到 rfc1867 为 http 协议添加了这个功能。当然在 rfc1867 中限定 form标签的 method 必须为 POST,enctype = "multipart/form-data" 以及<input type = "file">。 所以,当使用form表单上传文件的时候,请求头的content_type是multipart/form-data这种形式的,所以,我们需要在form标签添加enctype="multipart/form-data属性来进行标识。 如果你能打印上传文件的请求头,你会发现CONTENT_TYPE是这样的content_type:multipart/form-data; boundary=----WebKitFormBoundarylZZyJUkrgm6h34DU,那boundary=----WebKitFormBoundarylZZyJUkrgm6h34DU又是什么呢? 在multipart/form-data 后面有boundary以及一串字符,这是分界符,后面的一堆字符串是随机生成的,目的是防止上传文件中出现分界符导致服务器无法正确识别文件起始位置。那分界符又有啥用呢? 对于上传文件的post请求,我们没有使用原有的 http 协议,所以 multipart/form-data 请求是基于 http 原有的请求方式 post 而来的,那么来说说这个全新的请求方式与 post 的区别: 请求头的不同,对于上传文件的请求,contentType = multipart/form-data是必须的,而 post 则不是,毕竟 post 又不是只上传文件~。 请求体不同,这里的不同也就是指前者(上传文件请求)在发送的每个字段内容之间必须要使用分界符来隔开,比如文件的内容和文本的内容就需要分隔开,不然服务器就没有办法正常的解析文件,而后者 post 当然就没有分界符直接以key:value的形式发送就可以了。 当然,其中的弯弯绕绕不是三言两语能解释的清楚的,我们先知道怎么用就行。 再来看views视图处理: from django.shortcuts import render, redirect, HttpResponse from django.db import transaction def import_case(request, pk): """ 导入Excel数据,pk是所属项目的pk """ if request.method == 'POST': try: with transaction.atomic(): # 事物 # project_pk = request.POST.get("project_pk") # 数据库使用字段 excel = request.FILES.get('file_obj') book = xlrd.open_workbook(filename=None, file_contents=excel.read()) sheet = book.sheet_by_index(0) title = sheet.row_values(0) for row in range(1, sheet.nrows): print(sheet.row_values(row)) # 这里取出来每行的数据,就可以写入到数据库了 return HttpResponse('OK') except Exception as e: print(e) return render(request, 'import_case.html', {"project_pk": pk, "error": "上传文件类型有误,只支持 xls 和 xlsx 格式的 Excel文档"}) return render(request, 'import_case.html', {"project_pk": pk, "error": ""}) ajax上传文件# 前端文件: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!-- ajax上传文件开始 --> <div> {% csrf_token %} <input type="file" id="ajaxFile"> <button id="ajaxBtn">上传</button> </div> <!-- ajax上传文件结束 --> </body> <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> <script> console.log($("[name='csrfmiddlewaretoken']").val()); $("#ajaxBtn").click(function () { // 首先,实例化一个formdata对象 var formData = new FormData(); // 然后使用formdata的append来添加数据,即获取文件对象 // var file_obj = $("#ajaxFile")[0].files[0]; // 使用jQuery获取文件对象 var file_obj = document.getElementById('ajaxFile').files[0]; // 使用dom也行 formData.append('f1', file_obj ); // 处理csrftoken formData.append("csrfmiddlewaretoken", $("[name='csrfmiddlewaretoken']").val()); // 也可以将其他的数据,以键值对的形式,添加到formData中 formData.append('user','张开'); $.ajax({ url: "/upload/", type: "POST", data: formData, processData:false, // contentType:false, success:function (dataMsg) { console.log(dataMsg); } }) }) </script> </html> 在 ajax 中 contentType 设置为 false 是为了避免 JQuery 对请求头content_type进行操作,从而失去分界符,而使服务器不能正常解析文件。 在使用jQuery的$.ajax()方法的时候参数processData默认为true(该方法为jQuery独有的),默认情况下会将发送的数据序列化以适应默认的内容类型application/x-www-form-urlencoded 如果想发送不想转换的信息的时候需要手动将其设置为false即可。 再来看后端views.py如何处理: import xlrd from django.shortcuts import render from django.http import JsonResponse def upload(request): if request.is_ajax(): # print(request.META['CONTENT_TYPE']) # multipart/form-data; boundary=----WebKitFormBoundaryuXDgAwSKKIGnITam # print(request.POST) # <QueryDict: {'csrfmiddlewaretoken': ['mx1EBTtsOb0k96TUUW8XKbCGvK0Co3S6ZMlLvOuZOKAlO9nfhf6zol0V8KxRxbwT'], 'user': ['张开']}> # print(request.FILES) # <MultiValueDict: {'f1': [<InMemoryUploadedFile: 接口测试示例.xlsx (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)>]}> f1 = request.FILES.get('f1') print(f1) # 接口测试示例.xlsx book = xlrd.open_workbook(filename=None, file_contents=f1.read()) sheet = book.sheet_by_index(0) print(sheet.row_values(1)) # ['cnodejs项目', 'get /topics 主题首页', 'https://cnodejs.org/api/v1/topics', 'get', '', '{"success":true}'] # 如果还要保存文件的话 # with open(f1.name, 'wb') as f: # for line in f: # f.write(line) return JsonResponse({"message": "upload successful"}) else: return render(request, 'upload.html') OK了,多说无益,干就完了。 下载# 使用StreamingHttpResponse# views中主要代码: from django.http import StreamingHttpResponse def download(request): file=open('crm/models.py','rb') response =StreamingHttpResponse(file) response['Content-Type']='application/octet-stream' response['Content-Disposition']='attachment;filename="models.py"' return response 使用FileResponse# views中主要代码: from django.http import FileResponse def download(request): file=open('crm/models.py','rb') response =FileResponse(file) response['Content-Type']='application/octet-stream' response['Content-Disposition']='attachment;filename="models.py"' return response 解决filename不能有中文的问题# 如果你细心的尝试,会发现,上面两中下载方式中的filename不能包含中文,那么如何解决呢?来,往下看! from django.http import FileResponse from django.utils.encoding import escape_uri_path # 导入这个家伙 def download(request): file=open('crm/models.py','rb') response =FileResponse(file) response['Content-Type']='application/octet-stream' response['Content-Disposition']='attachment;filename="{}.py"'.format(escape_uri_path("我是中文啦")) return response 是不是解决了!完美!!
关于复选框的操作
获取所有的选中状态的复选框:

$("#chk1").find('input:checkbox').each(function() { //遍历所有复选框
if ($(this).prop('checked') == true) {
console.log($(this).val()); //打印当前选中的复选框的值
}
});
function getCheckBoxVal(){ //jquery获取所有选中的复选框的值
var chk_value =[];
$("#chk1").find('input[name="test"]:checked').each(function(){ //遍历,将所有选中的值放到数组中
chk_value.push($(this).val());
});
alert(chk_value.length==0 ?'你还没有选择任何内容!':chk_value);
}
或者:

$("#sure").click(function () { var arr = new Array(); $.each($(".p1"), function (index, item) { // console.log(index, item) if ($(item).get(0).checked) { arr.push($(item).val()) } }); if (arr.length == 0) { // 说明用户未选中用例,需要给提示 // console.log(2222222, "未选中", arr); $("#errorMsg").html("请勾选至少一个用例!"); } else { // 编写后续的操作 } });
批量执行用例:
前端序列化与反序列化
// 序列化 JSON.stringify(['A', 'B']) // 反序列化 JSON.parse()
ajax如何处理跨域问题?
有以下几种办法:
-
装饰器
# views中 from django.views.decorators.csrf import csrf_exempt @csrf_exempt def run_case(request, pk=0): pass
其他形式参考:https://www.cnblogs.com/Neeo/articles/11455271.html
list_api.html:

{% extends 'base.html' %} {% load static %} {% block breadcrumb %} <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="{% url 'index' %}">Index</a></li> <li class="breadcrumb-item active">{{ it_obj.it_name }}</li> <li class="breadcrumb-item active">用例列表</li> </ol> {% endblock %} {% block content %} {# {% if it_obj %}#} {# <div class="card card-primary">#} {# <div class="card-header">#} {# <a href="{% url 'add_it' %}">添加项目</a>#} {# </div>#} <table class="table table-striped table-hover table-bordered"> <thead> <tr> <th>选择</th> <th>序号</th> <th>名称</th> <th>描述</th> <th>请求url</th> <th>请求类型</th> <th>请求参数</th> <th>预期值</th> <th>用例报告</th> <th>通过状态</th> <th>是否执行</th> <td>操作</td> </tr> </thead> <tbody id="chk1"> {% for foo in api_obj %} <tr> <td> <input type="checkbox" value="{{ foo.pk }}" name="checkbox_list"> </td> <td>{{ forloop.counter }}</td> <td>{{ foo.api_name }}</td> <td title="{{ foo.api_desc }}">{{ foo.api_desc | truncatechars:10 }}</td> <td title="{{ foo.api_url }}">{{ foo.api_url | truncatechars:10 }}</td> <td>{{ foo.api_method }}</td> <td>{{ foo.api_params }}</td> <td>{{ foo.api_expect }}</td> {% if foo.api_report %} <td><a href="{% url 'download_case_report' foo.pk %}">下载</a></td> {% else %} <td>无</td> {% endif %} <td>{{ foo.get_api_pass_status_display }}</td> <td>{{ foo.get_api_run_status_display }}</td> <td> <a href="{% url 'delete_api' foo.pk %}" class="btn btn-danger btn-sm">删除</a> <a href="{% url 'edit_api' foo.pk %}" class="btn btn-info btn-sm">编辑</a> <a href="{% url 'run_case' foo.pk %}" class="btn btn-info btn-sm">执行</a> </td> </tr> {% endfor %} </tbody> </table> <div> {% csrf_token %} <input type="button" value="批量执行" class="btn btn-success" id="exeute" > </div> {# </div>#} {# {% else %}#} {# 没有数据,去 <a href="{% url 'add_it' %}">创建</a>#} {# {% endif %}#} {% endblock %} {% block js %} <script src="{% static 'sweetalert.min.js' %}"></script> <script> // 批量执行逻辑, /* 1. 点击执行按钮,获取到所有选中的CheckBox的值 - 点击后,没有找到选中的CheckBox,要给提示信息 2. 将多个值发送到后端 3. 后端处理完毕,返回前端状态 * */ $("#exeute").click(function () { var chk_value = new Array(); $.each($('input[name="checkbox_list"]:checked'), function (index, item) {//遍历,将所有选中的值放到数组中 console.log(item, index); chk_value.push($(item).val()); }); if (chk_value.length == 0) { swal({ "title": '没有勾选用例,请至少勾选一个用例后再点击执行!' }) } else { console.log(111111, chk_value); // ajax将 数组发送到后端 // 跨域 // 前端如何序列化 数组 JSON.stringify(chk_value) $.ajax({ "url": "{% url 'run_case' 0 %}", "type": "POST", "data": {"chk_value": JSON.stringify(chk_value), "csrfmiddlewaretoken":$("[name='csrfmiddlewaretoken']").val()}, success:function (data) { // console.log(data) window.location = data['path'] } }) } }); </script> {% endblock %}
base.html:(引入block js)

------------------ <!-- jQuery --> <script src="{% static 'AdminLTE-master/plugins/jquery/jquery.min.js' %}"></script> <!-- Bootstrap 4 --> <script src="{% static 'AdminLTE-master/plugins/bootstrap/js/bootstrap.bundle.min.js' %}"></script> <!-- AdminLTE App --> <script src="{% static 'AdminLTE-master/dist/js/adminlte.min.js' %}"></script> <script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script> {% block js %} {% endblock %} ------------------
io
在内存中创建一个文件句柄
from io import BytesIO from io import StringIO f = open('a.html', 'wb') # BytesIO 将文件写到内存里 f1 = open('a.html', 'w', encoding='utf-8') # StringIO
RequestHandler.py:

# -*- coding: utf-8 -*- # @Time : 2020/5/7 8:55 # @Author : 张开 # File : RequestHandler.py """ 处理请求相关和生成测试报告 """ import os import json import unittest import requests from io import BytesIO from deepdiff import DeepDiff from HTMLTestRunner import HTMLTestRunner from app01 import models from adminzdh.settings import BASE_DIR class MyCase(unittest.TestCase): def test_case(self): """ 用例 """ # 自定义描述信息 self._testMethodDoc = self.desc # 断言 (DeepDiff看看有没有字段被更改)(msg=self.msg可以自定义一些信息) self.assertEqual(DeepDiff(self.response, self.expect).get('type_changes', None), None, msg=self.msg) # 处理请求与数据库相关 class RequestOperate(object): def __init__(self, case_obj, suite_list): self.case_obj = case_obj self.suite_list = suite_list def handler(self): """ 关于请求的一些列流程: 1. 提取case_obj中的字段,使用requests发请求 1. 对请求参数进行校验 2. 将请求结果提取出来 2. 使用unittest进行断言 3. 更新数据库字段 4. 将执行结果添加到日志表中 5. 前端返回 """ # 发请求,断言,并生成测试报告 self.send_msg() def send_msg(self): """ 发请求 """ response = requests.request( method=self.case_obj.api_method, url=self.case_obj.api_url, data=self._check_data(), params=self._check_params(), ) self.assert_msg(response) def assert_msg(self, response): """ 处理断言 """ case = MyCase(methodName='test_case') case.response = response.json() case.expect = self._check_expect() case.msg = "自定义的错误信息: <hr>{}".format(DeepDiff(response.json(), self._check_expect())) case.title = self.case_obj.api_name case.desc = self.case_obj.api_desc # 将用例加载到批量执行的测试套件中TestSuite self.suite_list.addTest(case) # 创建一个当前的用例的测试套件对象 suite = unittest.TestSuite() # 添加进去 suite.addTest(case) # 执行单个的用例报告 self.create_single_report(suite) def create_single_report(self, suite): """ 生成单个用例报告 :param suite: 用例集 :return: """ # f = open(os.path.join(BASE_DIR, 'a.html'), 'wb') # 新进当前项目路径中 f = BytesIO() # 写进内存 # print(suite.countTestCases()) result = HTMLTestRunner( stream=f, # verbosity=2, title=self.case_obj.api_name, description=self.case_obj.api_desc, ).run(suite) # 更新数据相关字段的状态 self.update_api_status(result, f) def create_m_report(self, suite): """ 生成批量用例报告 :param suite: 用例集 :return: """ f = BytesIO() result = HTMLTestRunner( stream=f, # verbosity=2, title=self.case_obj.api_name, description=self.case_obj.api_desc, ).run(suite) # 更新log表 self.update_log_status(result, f) def update_log_status(self, result, f): """ 更新log表 """ log_data = {'pass': 0, "failed": 0, "total": 0, "errors": 0} for i in result.__dict__['result']: if i[0]: # 用例执行失败 log_data['failed'] += 1 else: log_data['pass'] += 1 log_data['total'] += 1 log_data['errors'] = result.__dict__['errors'].__len__() # 写log表,通过多少,失败多少,共执行了多少用例 models.Logs.objects.create( log_report=f.getvalue(), log_sub_it_id=self.case_obj.api_sub_it_id, log_pass_count=log_data['pass'], log_errors_count=log_data['errors'], log_failed_count=log_data['failed'], log_run_count=log_data["total"] ) def update_api_status(self, result, f): """ 更新数据相关字段的状态 1. api_report 2. api_run_time 3. api_pass_status 4. api_run_status """ # obj = models.Api.objects.filter(pk=self.case_obj.pk).update( # api_report=self.read_file(), # ) # 写报告 obj = models.Api.objects.filter(pk=self.case_obj.pk).first() obj.api_report = f.getvalue() # 写 执行时间 import datetime obj.api_run_time = datetime.datetime.now() # 写 api_run_status obj.api_run_status = 1 # 写 api_pass_status for i in result.__dict__['result']: if i[0]: # 用例执行失败 obj.api_pass_status = 0 else: obj.api_pass_status = 1 obj.save() def _check_expect(self): """ 处理预期值 """ if self.case_obj.api_expect: return json.loads(self.case_obj.api_expect) else: return {} def _check_data(self): """ 校验请求的 data 参数 : 默认,数据库中的data字段是标准的json串 """ if self.case_obj.api_data: # print(2222222222, json.loads(self.case_obj.api_data)) return json.loads(self.case_obj.api_data) else: return {} def _check_params(self): """ 校验请求的 params 参数 : 默认,数据库中的 params 字段是标准的json串 """ if self.case_obj.api_params: return json.loads(self.case_obj.api_params) else: return {} def run_case(api_list): """ 批量执行用例,单独执行用例也按照批量执行的来 :param api_list: :return: """ # print(api_list) # 在批量执行每一个用例之前创建一个suite_list,然后当批量执行中,执行每一个用例的时候,将封装好的用例对象添加到suite_list中 # 当批量执行执行完毕后,suite_list中,包含了所有批量执行的用例,此时,在使用HTMLTestRunner去生成一个批量执行的测试报告 # 创建一个测试套件(用个用例集合在一起)单个和多个测试用例都统一处理 suite_list = unittest.TestSuite() for i in api_list: # 执行RequestOperate执行用例流程 RequestOperate(case_obj=i, suite_list=suite_list).handler() # 问题,批量执行时,都能单独的执行并且生成各自的测试报告,但如何生成多个用例的报告呢?并且将该报告写入到log表中 # print(111111111, suite_list) # create_m_report上面的方法 生成批量的测试报告 RequestOperate(case_obj=i, suite_list=suite_list).create_m_report(suite_list)
views.py:

import json from django.shortcuts import render, HttpResponse, redirect from django.http import JsonResponse from app01 import models from utils.MyModelForm import ItModelForm, ApiModelForm from utils import RequestHandler def index(request): """ 项目主页 """ if request.method == "POST": return JsonResponse({"code": 0, "message": "项目主页的post请求,非法"}) else: it_obj = models.It.objects.all() # print(1111111, it_obj) return render(request, 'index.html', {"it_obj": it_obj}) def add_it(request): """ 添加项目 """ if request.method == "POST": form_data = ItModelForm(request.POST) if form_data.is_valid(): form_data.save() return redirect('/index/') else: return render(request, 'add_it.html', {"it_form_obj": form_data}) else: it_form_obj = ItModelForm() return render(request, 'add_it.html', {"it_form_obj": it_form_obj}) def edit_it(request, pk): """ 编辑项目, pk:项目的pk """ it_obj = models.It.objects.filter(pk=pk).first() if request.method == "POST": form_data = ItModelForm(request.POST, instance=it_obj) if form_data.is_valid(): form_data.save() return redirect('/index/') else: return render(request, 'add_it.html', {"it_form_obj": form_data}) else: it_form_obj = ItModelForm(instance=it_obj) return render(request, 'edit_it.html', {"it_form_obj": it_form_obj}) def delete_it(request, pk): """ 删除项目表记录,pk:项目的pk """ models.It.objects.filter(pk=pk).delete() return redirect('/index/') def list_api(request, pk): """ 思考:要不要有 pk ? pk:项目的pk 查看某一个项目下的用例列表 """ api_obj = models.Api.objects.filter(api_sub_it_id=pk) it_obj = models.It.objects.filter(pk=pk).first() # print(1111111, it_obj.it_name) return render(request, 'list_api.html', {"api_obj": api_obj, 'it_obj': it_obj}) def add_api(request, pk): """ 添加用例, pk:所属项目的pk """ if request.method == "POST": form_data = ApiModelForm(request.POST) if form_data.is_valid(): form_data.instance.__dict__['api_sub_it_id'] = pk # form_data.instance.api_sub_it = it_obj form_data.save() return redirect('/index/') else: return render(request, 'add_api.html', {"api_form_obj": form_data}) else: api_form_obj = ApiModelForm() it_obj = models.It.objects.filter(pk=pk).first() return render(request, 'add_api.html', {"api_form_obj": api_form_obj, "it_obj": it_obj}) def edit_api(request, pk): """ 编辑用例, pk:api的pk """ api_obj = models.Api.objects.filter(pk=pk).first() if request.method == "POST": form_data = ApiModelForm(request.POST, instance=api_obj) if form_data.is_valid(): # print(111111111, form_data.instance.__dict__) form_data.instance.__dict__['api_pass_status'] = 0 form_data.instance.__dict__['api_run_status'] = 0 form_data.instance.__dict__['api_report'] = "" form_data.save() return redirect('/list_api/{}'.format(api_obj.api_sub_it_id)) # 用例列表接口需要所属项目的pk值 else: return render(request, 'edit_api.html', {"api_form_obj": form_data}) else: api_form_obj = ApiModelForm(instance=api_obj) return render(request, 'edit_api.html', {"api_form_obj": api_form_obj, "it_obj": api_obj.api_sub_it}) def delete_api(request, pk): """ 删除用例, pk:用例的pk """ # 由于返回时,需要项目的pk值,这里不能直接删除 api_obj = models.Api.objects.filter(pk=pk).first() # 获取所属项目的pk it_obj_pk = api_obj.api_sub_it_id api_obj.delete() return redirect('/list_api/{}'.format(it_obj_pk)) # from django.views.decorators.csrf import csrf_exempt # # @csrf_exempt def run_case(request, pk=0): # ["11", "12"] """ 执行用例 """ # 如何做判断 请求 是ajax类型 # 批量执行 # if request.method == "POST": if request.is_ajax(): # 批量执行 chk_value = request.POST.get('chk_value') # 前端序列化的数据一定要记得反序列化回来 chk_value = json.loads(chk_value) # 数据库取 pk 在 chk_value 中记录对象(用例) api_list = models.Api.objects.filter(pk__in=chk_value) # print(api_list) # 对象交给RequestHandler处理 RequestHandler.run_case(api_list) # 跳转到日志页 return JsonResponse({"path": '/logs_list/'}) else: # 单个执行 case_obj = models.Api.objects.filter(pk=pk).first() # print(111111111, case_obj.api_data, type(case_obj.api_data)) RequestHandler.run_case([case_obj]) it_obj_pk = case_obj.api_sub_it_id # 跳转到日志页 return redirect('/logs_list/') from django.http import FileResponse from django.utils.encoding import escape_uri_path # 导入这个家伙 def download_case_report(request, pk): """ 下载用例的执行报告,pk:用例的pk """ api_obj = models.Api.objects.filter(pk=pk).first() # 下载 response = FileResponse(api_obj.api_report) response['Content-Type'] = 'application/octet-stream' response['Content-Disposition'] = 'attachment;filename="{}.{}"'.format(escape_uri_path(api_obj.api_name), 'html') return response def logs_list(request): """ log日志主页 """ if request.method == 'POST': return HttpResponse("ok") else: logs_obj = models.Logs.objects.all() return render(request, 'logs_list.html', {"logs_obj": logs_obj})
urls.py:

"""adminzdh URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.2/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path, re_path from app01 import views urlpatterns = [ path('admin/', admin.site.urls), path('', views.index, name="index"), path('index/', views.index, name="index"), path('add_it/', views.add_it, name='add_it'), re_path(r'^edit_it/(?P<pk>\d+)$', views.edit_it, name='edit_it'), re_path(r'^delete_it/(?P<pk>\d+)$', views.delete_it, name='delete_it'), # 接口用例表相关 re_path(r'^list_api/(?P<pk>\d+)$', views.list_api, name='list_api'), re_path(r'^add_api/(?P<pk>\d+)$', views.add_api, name='add_api'), re_path(r'^edit_api/(?P<pk>\d+)$', views.edit_api, name='edit_api'), re_path(r'^delete_api/(?P<pk>\d+)$', views.delete_api, name='delete_api'), # 执行用例 re_path(r'^run_case/(?P<pk>\d+)$', views.run_case, name='run_case'), # 下载用例执行报告 re_path(r'^download_case_report/(?P<pk>\d+)$', views.download_case_report, name='download_case_report'), # log日志 path('logs_list/', views.logs_list, name='logs_list'), ]
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具