一、项目需求:
每个测试项目下面有多个测试用例
1. 对测试项目的
. 增
. 删
. 改
. 查,查看该测试项目下面所有的测试用例
. 为该测试项目批量导入,添加测试用例
2. 对项目下的接口进行
. 增
. 删
. 改
. 查
. 单个用例的执行
. 批量执行选中的用例,并且将执行结果(html报告)下载到本地
3. 数据可视化
. 接口项目相关数据进行统计
. 用例执行情况进行统计
4. 定时任务
. 每个测试项目都有周期,在周期结束后,自动的将该项目中的所有用例,执行一遍,生成测试报告。
. 使用Django发邮件功能,将报告发送
5、相关功能截图
①整体流程
②接口项目列表
③为指定的接口测试项目批量导入
④为指定的接口测试项目添加用例
⑤某个接口测试项目下的用例列表
二、项目框架搭建:
1. 创建框架起始目录结构
2.settings里面配置static
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
3.创建static文件夹并引入AdminLTE-master前端框架、bootstrap、echarts、jquery
4. base模板继承static里面的css、jpg、png、修改href、注释没用页面、自定义container
①修改前的base.html
②要修改的base.html位置
5. index模板继承base模板重写content-header块、content块
6. 配置urls
from app01 import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^index/', views.index, name='index'),
]
7. 视图views
from django.shortcuts import render
# 查看数据:
from app01 import models
def index(request):
项目主页功能:
if request.method == "POST":
pass
else:
it_obj = models.Interface.objects.all()
return render(request, "index.html", {"it_obj": it_obj})
8. 先跑下看看页面
三、项目具体实现:
1. 表结构设计
用例表和接口项目表是多对一的关系。
日志表和接口项目表是多对一的关系。
from django.db import models
class Interface(models.Model):
接口项目表:
接口项目名称') # 长整型
接口项目描述') # 文本类型
项目开始时间') # DateField是日期、DateTimeField是日期时间
项目开始时间')
def __str__(self):
返回重定向字符串:
return self.project_title
def zhangyu(self):
当项目下有用例的时候:
if self.case_set.count():
return "{}%".format(self.case_set.filter(case_pass_status=1).count() / self.case_set.count() * 100)
else:
return 0
class Meta:
ordering = ['project_start_time']
class Case(models.Model):
用例表:
所属接口') # ForeignKey外键关联接口项目表
用例名称')
用例描述')
请求类型') # 该字段也可以设置为choices字段,让前端下拉选择
请求URL')
用例的参数', default='') # 在前段输入完整的json串
预期值')
case_execute_status = models.IntegerField(choices=(
已执行'),
未执行'),
整型
case_pass_status = models.IntegerField(choices=(
已通过'),
未通过'),
), default=2)
用例执行报告', default='')
用例执行时间', auto_now_add=True) # auto_now_add前端控制显示
def __str__(self):
return self.case_title
class Log(models.Model):
日志表:
日志所属的接口项目')
测试执行报告')
创建时间', auto_now_add=True)
class Meta:
ordering = ['-log_create_time']
①接口项目表
接口项目名称
接口项目描述
项目开始时间
项目结束时间
②用例表
用例所属的接口项目,外键
用例名称
用例描述
用例的请求类型
用例的请求url
用例的请求参数
预期值
执行状态:已执行和未执行
通过状态:未通过和已通过
用例的执行结果报告
执行时间
2. 数据库迁移
①:makemigrations app01
②:migrate app01
3. index.html主页功能
# 导入日期:
import datetime
import json
# 导入处理文件模块:
import xlrd
from django.shortcuts import render, redirect, HttpResponse
# 查看数据:
from app01 import models
# 导入form:
from util import MyForm
# 导入django事务模块:
from django.db import transaction
# 导入JsonResponse:
from django.http import JsonResponse
# 导入用例处理功能:
from util import ExecuteCaseHandler
# 导入处理数据流响应:
from django.http import StreamingHttpResponse
from django.utils.encoding import escape_uri_path
# 导入FileResponse:
from django.http import FileResponse
# 导入处理表格功能:
from util import ShowTabHandler
def index(request):
# 项目主页功能:
# django创建日期:
# models.Interface.objects.create(
# project_title="项目3",
# project_desc="项目3的描述",
# project_start_time=datetime.datetime.date(datetime.datetime.now()),
# project_end_time=datetime.datetime.date(datetime.datetime.now()),
# )
if request.method == "POST":
pass
else:
it_obj = models.Interface.objects.all()
return render(request, "index.html", {"it_obj": it_obj})
{#继承base模板:#}
{% extends 'base.html' %}
{#重写content-header块:#}
{% block content-header %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="#">Home</a></li>
<li class="breadcrumb-item active" aria-current="page">项目列表</li>
</ol>
</nav>
{% endblock %}
{#重写conten块:#}
{% block content %}
<div class="content">
<div class="container-fluid">
<div class="row">
{# class="col-lg-12"调框:#}
<div class="col-lg-12">
<div class="card">
<div class="card-header">
<h5 class="m-0">
<nav>
{# 链接到add_interface:#}
<a href="{% url 'add_interface' %}">创建项目</a>
</nav>
</h5>
</div>
<div class="card-body">
{# 渲染数据:#}
{% if it_obj %}
{# 建立表格:#}
<table class="table table-striped">
{# 建立表头:#}
<thead>
<tr>
<th>序号</th>
<th>项目名称</th>
<th>项目描述</th>
<th>用例数量</th>
<th>覆盖率</th>
<th>开始时间</th>
<th>结束时间</th>
<th>操作</th>
</tr>
</thead>
{# 建立表体:#}
<tbody>
{% for foo in it_obj %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ foo.project_title }}</td>
<td>{{ foo.project_desc }}</td>
<td>{{ foo.case_set.count }}</td>
<!-- 计算公式:通过/用例总数 -->
<td>{{ foo.zhangyu }}</td>
<td>{{ foo.project_start_time | date:"Y-m-d" }}</td>
<td>{{ foo.project_end_time | date:"Y-m-d" }}</td>
<td>
{# 确定删除哪一个加foo.pk:#}
<a href="{% url 'del_interface' foo.pk %}" class="btn btn-danger btn-sm">删除项目</a>
<a href="{% url 'edit_interface' foo.pk %}" class="btn btn-default btn-sm">编辑项目</a>
<a href="{% url 'case_list' foo.pk %}" class="btn btn-success btn-sm">查看用例</a>
<a href="{% url 'add_case' foo.pk %}" class="btn btn-warning btn-sm">添加用例</a>
<a href="{% url 'import_excel' foo.pk %}" class="btn btn-dark btn-sm">批量导入</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
暂时还没有项目,去<a href="{% url 'add_interface' %}">创建项目</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
4. add_interface.html添加接口项目功能(使用form):
def add_interface(request):
# 添加接口项目功能:
if request.method == 'POST':
form_obj = MyForm.InterfaceModelForm(request.POST)
if form_obj.is_valid():
form_obj.save()
return redirect('/index/')
else:
return render(request, 'add_interface.html', {"form_obj": form_obj})
else:
# 使用InterfaceModelForm:
form_obj = MyForm.InterfaceModelForm()
return render(request, "add_interface.html", {"form_obj": form_obj})
{% extends 'base.html' %}
{% block content-header %}
{% endblock %}
{% block content %}
<div class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-header">
<h5 class="m-0">
<nav>
<a href="{% url 'index' %}">返回项目列表页</a>
</nav>
</h5>
</div>
<div class="card-body">
{# 使用MyForm里面的InterfaceModelForm、POST请求#}
<form action="" method="POST" novalidate>
{% csrf_token %}
{% for foo in form_obj %}
<div>
<label for="">{{ foo.label }}</label>
{{ foo }}
<span style="color:red;">{{ foo.errors.0 }}</span>
</div>
{% endfor %}
<div>
<input type="submit" value="提交" class="btn btn-success">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
5. del_interface删除接口项目功能
def del_interface(request, pk):
# 删除项目接口功能 pk:项目记录的pk值:
models.Interface.objects.filter(pk=pk).delete()
return redirect('/index/')
6. edit_interface编辑接口项目功能
def edit_interface(request, pk):
# 编辑项目接口功能 pk:项目记录的pk
obj = models.Interface.objects.filter(pk=pk).first()
if request.method == "POST":
form_obj = MyForm.InterfaceModelForm(request.POST, instance=obj)
if form_obj.is_valid():
form_obj.save()
return redirect('/index/')
else:
return render(request, 'edit_interface.html', {"form_obj": form_obj})
else:
form_obj = MyForm.InterfaceModelForm(instance=obj)
return render(request, 'edit_interface.html', {"form_obj": form_obj})
{% extends 'base.html' %}
{% block content-header %}
{% endblock %}
{% block content %}
<div class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-header">
<h5 class="m-0">
<nav>
<a href="{% url 'index' %}">返回项目列表页</a>
</nav>
</h5>
</div>
<div class="card-body">
{# 使用MyForm里面的InterfaceModelForm、POST请求#}
<form action="" method="POST" novalidate>
{% csrf_token %}
{% for foo in form_obj %}
<div>
<label for="">{{ foo.label }}</label>
{{ foo }}
<span style="color:red;">{{ foo.errors.0 }}</span>
</div>
{% endfor %}
<div>
<input type="submit" value="提交" class="btn btn-success">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
7. case_list用例列表展示功能
def case_list(request, pk):
# 展示项目下所有的用例列表,pk:项目记录的pk
if request.method == "POST":
pass
else:
obj = models.Case.objects.filter(case_sub_it__pk=pk)
it_obj = models.Interface.objects.filter(pk=pk).first()
return render(request, 'case_list.html', {"case_list_obj": obj, "it_obj": it_obj})
{% extends 'base.html' %}
{#面包屑导航#}
{% block content-header %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'index' %}">项目列表</a></li>
<li class="breadcrumb-item"><a href="{% url 'case_list' it_obj.pk %}">{{ it_obj.project_title }}</a></li>
<li class="breadcrumb-item active" aria-current="page">用例列表</li>
</ol>
</nav>
{% endblock %}
{% block content %}
<div class="content">
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-header">
<h5 class="m-0">
<nav>
<a href="{% url 'index' %}">返回项目列表页</a>
</nav>
</h5>
</div>
<div class="card-body">
<form action="{% url 'execute_case' %}" method="post">
{% csrf_token %}
{% if case_list_obj %}
<table class="table table-striped">
<thead>
<tr>
<th>选择</th>
<th>序号</th>
<th>名称</th>
<th>描述</th>
<th>所属项目</th>
<th>URL</th>
<th>请求类型</th>
<th>期望值</th>
<th>执行状态</th>
<th>通过状态</th>
<th>报告</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for foo in case_list_obj %}
<tr>
<td><input type="checkbox" value="{{ foo.pk }}" name="case_pk" class="p1"></td>
<td>{{ forloop.counter }}</td>
<td>{{ foo.case_title }}</td>
<td title="{{ foo.case_desc }}">{{ foo.case_desc | truncatechars:10 }}</td>
<td title="{{ foo.case_sub_it.project_title }}">{{ foo.case_sub_it.project_title }}</td>
<td title="{{ foo.case_url }}">{{ foo.case_url | truncatechars:20 }}</td>
<td>{{ foo.case_method }}</td>
<td>{{ foo.case_expect | truncatechars:20 }}</td>
<td>{{ foo.get_case_execute_status_display }}</td>
<td>{{ foo.get_case_pass_status_display }}</td>
<td>
{% if foo.case_report == '' %}
无
{% else %}
<a href="{% url 'download_case_report' foo.pk %}" download>下载</a>
{% endif %}
</td>
<td>
<a href="{% url 'del_case' foo.pk %}" class="btn btn-danger btn-sm">删除</a>
<a href="{% url 'edit_case' foo.pk %}" class="btn btn-default btn-sm">编辑</a>
<a href="{% url 'execute_case'%}" class="btn btn-warning btn-sm">执行</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
[{{ it_obj.project_title }}]下暂时还没有用例,去<a href="{% url 'add_case' it_obj.pk %}">创建</a>
{% endif %}
<input type="button" value="批量执行并下载报告" class="btn btn-success" id="sure">
<span id="errorMsg" style="color: red;"></span>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="https://cdn.bootcss.com/sweetalert/2.1.2/sweetalert.min.js"></script>
<script>
$("#sure").click(function () {
var arr = new Array();
$.each($(".p1"), function (index, item) {
if ($(item).get(0).checked) {
arr.push($(item).val())
}
});
if (arr.length == 0) {
// 说明用户未选中用例,需要给提示
// console.log(2222222, "未选中", arr);
$("#errorMsg").html("请勾选至少一个用例!")
} else {
swal({
title: "Successful",
text: "用例正在执行",
timer: 20000,
showConfirmButton: false
});
$.ajax({
url: "/execute_case/",
type: "POST",
data: {
"case_list": JSON.stringify(arr),
"csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val()
},
success: function (dataMsg) {
window.location.href = '/crontab_log/';
}
})
}
});
</script>
{% endblock %}
8. del_case删除用例功能
def del_case(request, pk):
# 删除用例记录,pk:用例的pk
# 首先从表中将case对象取出来,
case_obj = models.Case.objects.filter(pk=pk).first()
# 因为后续的返回需要,case对象所属项目的pk值,所以,我们先把该pk值拿到
interface_obj_pk = case_obj.case_sub_it.pk
# 然后在执行删除
case_obj.delete()
return redirect('/case_list/{}'.format(interface_obj_pk)) # 需要所属项目的pk值
9. edit_case编辑用例功能
def edit_case(request, pk):
# 编辑用例,pk:用例的pk
case_obj = models.Case.objects.filter(pk=pk).first()
if request.method == "POST":
# 编辑用例返回之前恢复下状态:
case_obj.case_execute_status = 2
case_obj.case_pass_status = 2
case_obj.case_report = ""
form_obj = MyForm.CaseModelForm(request.POST, instance=case_obj)
if form_obj.is_valid():
form_obj.save()
return redirect('/case_list/{}'.format(case_obj.case_sub_it_id))
else:
return render(request, 'edit_case.html', {"form_obj": form_obj, "it_obj": case_obj})
else:
form_obj = MyForm.CaseModelForm(instance=case_obj)
return render(request, 'edit_case.html', {"form_obj": form_obj, "it_obj": case_obj})
10. 面包屑导航功能
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
项目列表</a></li>
<li class="breadcrumb-item"><a href="{% url 'case_list' it_obj.pk %}">{{ it_obj.project_title }}</a></li>
用例列表</li>
</ol>
</nav>
11. add_case添加用例功能
def add_case(request, pk):
# 为指定的项目添加一条记录 pk:接口项目的pk
it_obj = models.Interface.objects.filter(pk=pk).first()
if request.method == "POST":
form_obj = MyForm.CaseModelForm(request.POST)
if form_obj.is_valid():
form_obj.save()
return redirect('/case_list/{}'.format(pk))
else:
return render(request, 'add_case.html', {"form_obj": form_obj, "it_obj": it_obj})
else:
form_obj = MyForm.CaseModelForm()
return render(request, 'add_case.html', {"form_obj": form_obj, "it_obj": it_obj})
12. import_excel用例批量导入功能
def import_excel(request, pk):
# 为指定的项目,批量导入用例,用例来自Excel表格,pk:项目记录的pk
obj = models.Interface.objects.filter(pk=pk).first()
if request.method == "POST":
try:
with transaction.atomic(): # 事物处理
res1 = request.FILES.get('it_file')
book = xlrd.open_workbook(file_contents=res1.read())
sheet = book.sheet_by_index(0)
for row in range(1, sheet.nrows):
row = sheet.row_values(row)
models.Case.objects.create(
case_sub_it_id=pk,
case_title=row[0],
case_desc=row[1],
case_url=row[2],
case_method=row[3],
case_params=row[4],
case_expect=row[5],
)
return redirect('/case_list/{}'.format(pk))
except Exception as e:
return render(request, 'import_excel.html', {"it_obj": obj, "error_msg": "上传的文件类型只能是 [xlsx] 或者 [xls] 类型的文件,报错详细:{}".format(e)})
else:
return render(request, 'import_excel.html', {"it_obj": obj})
13. execute_case单个用例执行功能
def execute_case(request):
"""
执行单个用例 ,pk:用例的pk
1. 从前端获取所有记录的pk
2. 从数据库将该记录对象查询出来
3. 循环从对象中提取相关的参数,发requests请求,断言
4. 使用unittest生成测试报告
5. 更新数据库字段
6. 给前端一个反馈
"""
if request.method == "POST":
# 前端的数据是序列化后的,所以,后端先要反序列化:
case_list_pk = json.loads(request.POST.get('case_list'))
# 从数据库中匹配出来用例对象:
case_list = models.Case.objects.filter(pk__in=case_list_pk)
# 循环执行用例对象,断言
f = ExecuteCaseHandler.run_case(case_list)
return JsonResponse({"STATUS": "OK"})
else:
return JsonResponse({"code": 0, "message": "非法的请求方式"})
14. 批量执行功能
15. 定时任务功能
def crontab_log(request):
# 批量执行和定时任务的日志列表页:
if request.method == 'POST':
return HttpResponse("定时任务页面")
else:
log_obj = models.Log.objects.all()
return render(request, 'crontab_log_list.html', {"log_obj": log_obj})
16. django发邮件功能
17. 点击功能增加样式
18. 下载报告功能
19. 临时文件优化BytesIO用法
20. 批量日志功能
21. 多线程开启定时任务功能
22. 可视化功能
①折线图
②饼图
23.用例编辑的bug处理
四、项目中遇到的问题:
1. orm中关于日期类型的字段前端不展示
不要手动在pycharm中自己去添加日期类型的记录,数据会转成时间戳类型的时间,在前端无法渲染。
2.用例编辑的bug处理