• 使用unittest和Django搭配写一个接口测试平台


    一、项目需求:

    每个测试项目下面有多个测试用例

    1. 对测试项目的

       ①.

       ②.

       ③.

       ④. 查,查看该测试项目下面所有的测试用例

       ⑤. 为该测试项目批量导入,添加测试用例

    2. 对项目下的接口进行

       ①.

       ②.

       ③.

       ④.

       ⑤. 单个用例的执行

       ⑥. 批量执行选中的用例,并且将执行结果(html报告)下载到本地

    3. 数据可视化

       ①. 接口项目相关数据进行统计

       ②. 用例执行情况进行统计

    4. 定时任务

       ①. 每个测试项目都有周期,在周期结束后,自动的将该项目中的所有用例,执行一遍,生成测试报告。

       ②. 使用Django发邮件功能,将报告发送

    5、相关功能截图

    ①整体流程

    ②接口项目列表

     

    ③为指定的接口测试项目批量导入

     

    ④为指定的接口测试项目添加用例

     

    ⑤某个接口测试项目下的用例列表

     

    二、项目框架搭建:

    1. 创建框架起始目录结构

     

    2.settings里面配置static

    STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

     

    3.创建static文件夹并引入AdminLTE-master前端框架、bootstrapechartsjquery

     

    4. base模板继承static里面的cssjpgpng、修改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):

        # 接口项目表:

        project_title = models.CharField(max_length=32, verbose_name='接口项目名称')  # 长整型

        project_desc = models.TextField(max_length=255, verbose_name='接口项目描述')  # 文本类型

        project_start_time = models.DateField(verbose_name='项目开始时间')    # DateField是日期、DateTimeField是日期时间

        project_end_time = models.DateField(verbose_name='项目开始时间')

        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):

        # 用例表:

        case_sub_it = models.ForeignKey(to='Interface', verbose_name='所属接口')  # ForeignKey外键关联接口项目表

        case_title = models.CharField(max_length=32, verbose_name='用例名称')

        case_desc = models.CharField(max_length=255, verbose_name='用例描述')

        case_method = models.CharField(max_length=12, verbose_name='请求类型')  # 该字段也可以设置为choices字段,让前端下拉选择

        case_url = models.CharField(max_length=255, verbose_name='请求URL')

        case_params = models.CharField(max_length=255, verbose_name='用例的参数', default='')  # 在前段输入完整的json

        case_expect = models.CharField(max_length=255, verbose_name='预期值')

        case_execute_status = models.IntegerField(choices=(

            (1, '已执行'),

            (2, '未执行'),

        ), default=2)   # IntegerField整型

        case_pass_status = models.IntegerField(choices=(

            (1, '已通过'),

            (2, '未通过'),

        ), default=2)

        case_report = models.TextField(verbose_name='用例执行报告', default='')

        case_execute_time = models.DateTimeField(verbose_name='用例执行时间', auto_now_add=True)  # auto_now_add前端控制显示

        def __str__(self):

            return self.case_title

    class Log(models.Model):

        # 日志表:

        log_sub_it = models.ForeignKey(to='Interface', verbose_name='日志所属的接口项目')

        log_report = models.TextField(verbose_name='测试执行报告')

        log_create_time = models.DateTimeField(verbose_name='创建时间', 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">

                <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>

    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处理

  • 相关阅读:
    如何选择合适的开源消息中间件
    使用Rest访问Redis中的数据
    论消息队列在分布式系统的重要性
    grub-install: warning: this GPT partition label contains no BIOS Boot Partition; embedding won’t be possible Ubuntu使用BIOS启动时, GPT分区表下安装grub2报错 的解决办法
    Linux Ubuntu 16.04 启动后 桌面崩溃
    Linux Ubuntu 1604 grub2 rescue mod 启动
    EF自动探测更改
    C# 使用OracleParameter传递参数提示缺少表达式
    Gitlab安装后 500 错误 PostGre数据库无法启动
    DevExpress GridControl GridView多选状态下,代码赋值FocusedRowHandle,样式无变化
  • 原文地址:https://www.cnblogs.com/zhang-da/p/12608087.html
Copyright © 2020-2023  润新知