用力执行日志-可视化-定时任务

日志页面优化

base.html(修改菜单标题):

<!DOCTYPE html>
<!--
This is a starter template page. Use this page to start your new project from
scratch. This page gets rid of all links and provides the needed markup only.
-->
<html lang="en">
<head>
    {% load static %}
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="x-ua-compatible" content="ie=edge"><title>邓鑫</title><!-- Font Awesome Icons -->
    <link rel="stylesheet" href="{% static 'AdminLTE-master/plugins/fontawesome-free/css/all.min.css' %}">
    <!-- Theme style -->
    <link rel="stylesheet" href="{% static 'AdminLTE-master/dist/css/adminlte.min.css' %}">
{#    <link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">#}
    <!-- Google Font: Source Sans Pro -->
{#    <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet">#}
</head>
<body class="hold-transition sidebar-mini">
<div class="wrapper"><!-- Navbar -->
    <nav class="main-header navbar navbar-expand navbar-white navbar-light">
        <!-- Left navbar links -->
        <ul class="navbar-nav">
            <li class="nav-item">
                <a class="nav-link" data-widget="pushmenu" href="#"><i class="fas fa-bars"></i></a>
            </li>
            <li class="nav-item d-none d-sm-inline-block">
                <a href="{% static '/AdminLTE-master/index3.html' %}" class="nav-link">Home</a>
            </li>
            <li class="nav-item d-none d-sm-inline-block">
                <a href="#" class="nav-link">Contact</a>
            </li>
        </ul><!-- SEARCH FORM -->
        <form class="form-inline ml-3">
            <div class="input-group input-group-sm">
                <input class="form-control form-control-navbar" type="search" placeholder="Search" aria-label="Search">
                <div class="input-group-append">
                    <button class="btn btn-navbar" type="submit">
                        <i class="fas fa-search"></i>
                    </button>
                </div>
            </div>
        </form><!-- Right navbar links -->
        <ul class="navbar-nav ml-auto">
            <!-- Messages Dropdown Menu -->
            <li class="nav-item dropdown">
                <a class="nav-link" data-toggle="dropdown" href="#">
                    <i class="far fa-comments"></i>
                    <span class="badge badge-danger navbar-badge">3</span>
                </a>
                <div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
                    <a href="#" class="dropdown-item">
                        <!-- Message Start -->
                        <div class="media">
                            <img src="{% static 'AdminLTE-master/dist/img/user1-128x128.jpg' %}" alt="User Avatar"
                                 class="img-size-50 mr-3 img-circle">
                            <div class="media-body">
                                <h3 class="dropdown-item-title">
                                    Brad Diesel
                                    <span class="float-right text-sm text-danger"><i class="fas fa-star"></i></span>
                                </h3>
                                <p class="text-sm">Call me whenever you can...</p>
                                <p class="text-sm text-muted"><i class="far fa-clock mr-1"></i> 4 Hours Ago</p>
                            </div>
                        </div>
                        <!-- Message End -->
                    </a>
                    <div class="dropdown-divider"></div>
                    <a href="#" class="dropdown-item">
                        <!-- Message Start -->
                        <div class="media">
                            <img src="{% static 'AdminLTE-master/dist/img/user8-128x128.jpg' %}" alt="User Avatar"
                                 class="img-size-50 img-circle mr-3">
                            <div class="media-body">
                                <h3 class="dropdown-item-title">
                                    John Pierce
                                    <span class="float-right text-sm text-muted"><i class="fas fa-star"></i></span>
                                </h3>
                                <p class="text-sm">I got your message bro</p>
                                <p class="text-sm text-muted"><i class="far fa-clock mr-1"></i> 4 Hours Ago</p>
                            </div>
                        </div>
                        <!-- Message End -->
                    </a>
                    <div class="dropdown-divider"></div>
                    <a href="#" class="dropdown-item">
                        <!-- Message Start -->
                        <div class="media">
                            <img src="{% static 'AdminLTE-master/dist/img/user3-128x128.jpg' %}" alt="User Avatar"
                                 class="img-size-50 img-circle mr-3">
                            <div class="media-body">
                                <h3 class="dropdown-item-title">
                                    Nora Silvester
                                    <span class="float-right text-sm text-warning"><i class="fas fa-star"></i></span>
                                </h3>
                                <p class="text-sm">The subject goes here</p>
                                <p class="text-sm text-muted"><i class="far fa-clock mr-1"></i> 4 Hours Ago</p>
                            </div>
                        </div>
                        <!-- Message End -->
                    </a>
                    <div class="dropdown-divider"></div>
                    <a href="#" class="dropdown-item dropdown-footer">See All Messages</a>
                </div>
            </li>
            <!-- Notifications Dropdown Menu -->
            <li class="nav-item dropdown">
                <a class="nav-link" data-toggle="dropdown" href="#">
                    <i class="far fa-bell"></i>
                    <span class="badge badge-warning navbar-badge">15</span>
                </a>
                <div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
                    <span class="dropdown-header">15 Notifications</span>
                    <div class="dropdown-divider"></div>
                    <a href="#" class="dropdown-item">
                        <i class="fas fa-envelope mr-2"></i> 4 new messages
                        <span class="float-right text-muted text-sm">3 mins</span>
                    </a>
                    <div class="dropdown-divider"></div>
                    <a href="#" class="dropdown-item">
                        <i class="fas fa-users mr-2"></i> 8 friend requests
                        <span class="float-right text-muted text-sm">12 hours</span>
                    </a>
                    <div class="dropdown-divider"></div>
                    <a href="#" class="dropdown-item">
                        <i class="fas fa-file mr-2"></i> 3 new reports
                        <span class="float-right text-muted text-sm">2 days</span>
                    </a>
                    <div class="dropdown-divider"></div>
                    <a href="#" class="dropdown-item dropdown-footer">See All Notifications</a>
                </div>
            </li>
            <li class="nav-item">
                <a class="nav-link" data-widget="control-sidebar" data-slide="true" href="#"><i
                        class="fas fa-th-large"></i></a>
            </li>
        </ul>
    </nav>
    <!-- /.navbar --><!-- Main Sidebar Container -->
    <aside class="main-sidebar sidebar-dark-primary elevation-4">
        <!-- Brand Logo -->
        <a href="{% static 'AdminLTE-master/index3.html' %}" class="brand-link">
            <img src="{% static 'AdminLTE-master/dist/img/AdminLTELogo.png' %}" alt="AdminLTE Logo"
                 class="brand-image img-circle elevation-3"
                 style="opacity: .8">
            <span class="brand-text font-weight-light">AdminLTE 3</span>
        </a><!-- Sidebar -->
        <div class="sidebar">
            <!-- Sidebar user panel (optional) -->
            <div class="user-panel mt-3 pb-3 mb-3 d-flex">
                <div class="image">
                    <img src="{% static 'AdminLTE-master/dist/img/user2-160x160.jpg' %}" class="img-circle elevation-2"
                         alt="User Image">
                </div>
                <div class="info">
                    <a href="#" class="d-block">Alexander Pierce</a>
                </div>
            </div><!-- Sidebar Menu -->
            <nav class="mt-2">
                <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu"
                    data-accordion="false">
                    <li class="nav-item">
                        <a href="#" class="nav-link">
                            <i class="nav-icon fas fa-th"></i>
                            <p>
                                项目列表
                                <span class="right badge badge-danger">New</span>
                            </p>
                        </a>
                    </li>
                    <li class="nav-item">
                        <a href="#" class="nav-link">
                            <i class="nav-icon fas fa-th"></i>
                            <p>
                                log日志
                                <span class="right badge badge-danger">New</span>
                            </p>
                        </a>
                    </li>
                    <li class="nav-item">
                        <a href="#" class="nav-link">
                            <i class="nav-icon fas fa-th"></i>
                            <p>
                                可视图表
                                <span class="right badge badge-danger">New</span>
                            </p>
                        </a>
                    </li>
                </ul>
            </nav>
            <!-- /.sidebar-menu -->
        </div>
        <!-- /.sidebar -->
    </aside><!-- Content Wrapper. Contains page content -->
    <div class="content-wrapper">
        <!-- Content Header (Page header) -->
        <div class="content-header">
            <div class="container-fluid">
                <div class="row mb-2">
                    <div class="col-sm-6">
                        {% block breadcrumb %}
                        <ol class="breadcrumb">
                            <li class="breadcrumb-item"><a href="#">Home</a></li>
                            <li class="breadcrumb-item active">Starter Page</li>
                        </ol>
                        {% endblock %}
                        {#            <h1 class="m-0 text-dark">Starter Page</h1>#}
                        
                    </div><!-- /.col -->
                </div><!-- /.row -->
            </div><!-- /.container-fluid -->
        </div>
        <!-- /.content-header --><!-- Main content -->
        <div class="content">
            <div class="container-fluid">
                <div class="row">
                    <div class="col-lg-12">
​
                        {% block content %}
                            <div class="card card-primary card-outline">
                            <div class="card-header"><h5 class="m-0">title</h5></div>
                            <div class="card-body">
                            </div>
                        </div>
                        {% endblock %}
​
                    </div>
                    <!-- /.col-md-6 -->
                </div>
                <!-- /.row -->
            </div><!-- /.container-fluid -->
        </div>
        <!-- /.content -->
    </div>
    <!-- /.content-wrapper --><!-- Control Sidebar -->
    <aside class="control-sidebar control-sidebar-dark">
        <!-- Control sidebar content goes here -->
        <div class="p-3">
            <h5>Title</h5>
            <p>Sidebar content</p>
        </div>
    </aside>
    <!-- /.control-sidebar --><!-- Main Footer -->
    <footer class="main-footer">
        <!-- To the right -->
        <div class="float-right d-none d-sm-inline">
            Anything you want
        </div>
        <!-- Default to the left -->
        <strong>Copyright &copy; 2014-2019 <a href="https://adminlte.io">AdminLTE.io</a>.</strong> All rights reserved.
    </footer>
</div>
<!-- ./wrapper --><!-- REQUIRED SCRIPTS --><!-- 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 %}
​
</body>
</html>
View Code

logs_list.html(日志页):

{% extends 'base.html' %}
​
​
​
{% block content %}
​
​
    <table class="table table-striped table-hover table-bordered">
        <thead>
        <tr>
            <th>序号</th>
            <th>所属项目</th>
            <th>创建时间</th>
            <th>执行用例总数</th>
            <th>用例失败数</th>
            <th>用例报错数</th>
            <th>用例通过数</th>
            <th>通过率</th>
            <th>用例报告</th>
        </tr>
        </thead>
        <tbody>
        {% for foo in logs_obj %}
            <tr>
                <td>{{ forloop.counter }}</td>
                <td>{{ foo.log_sub_it.it_name }}</td>
                <td>{{ foo.log_run_time }}</td>
                <td>{{ foo.log_run_count }}</td>
                <td>{{ foo.log_failed_count }}</td>
                <td>{{ foo.log_errors_count }}</td>
                <td>{{ foo.log_pass_count }}</td>
                <td>{{ foo.pass_rate }}</td>
{#                <td><a href="{% url 'preview' foo.pk %}">预览</a></td>#}
            </tr>
        {% endfor %}
​
        </tbody>
    </table>
​
{% endblock %}
View Code

 

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})
View Code

 

urls.py:

''''
# log日志
url(r'^logs_list/', views.logs_list, name='logs_list'),
url(r'^show_tab/', views.show_tab, name='show_tab'),
url(r'^preview/(?P<pk>\d+)$', views.preview, name='preview'),
''''
View Code

 

models.py:

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 "%.2f%%" % 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:
            result = self.log_pass_count / self.log_run_count * 100
            if result:
                return "%.f%%" % (result)
            else:
                return 0
        else:
            return 0
​
    class Meta:
        ordering = ['-log_run_time']
View Code

 

日志预览:

urls.py:

urlpatterns = [
.......

    # log日志
    path('logs_list/', views.logs_list, name='logs_list'),
    path('^show_tab/', views.show_tab, name='show_tab'),
    re_path(r'^preview/(?P<pk>\d+)$', views.preview, name='preview'),

]
View Code

 

views.py:

.......

def preview(request, pk):
    """ 测试报告预览 , pk:logs记录的pk"""
    if request.method == "POST":
        report_pk = request.POST.get("report_pk")
        log_obj = models.Logs.objects.filter(pk=report_pk).first()
        # 下载
        response = FileResponse(log_obj.log_report)
        response['Content-Type'] = 'application/octet-stream'
        response['Content-Disposition'] = 'attachment;filename="{}.{}"'.format(
            escape_uri_path(log_obj.log_sub_it.it_name),
            'html')
        return response
    log_obj = models.Logs.objects.filter(pk=pk).first()
    return render(request, 'preview.html', {"log_obj": log_obj})
View Code

 

preview.html:

{% extends 'base.html' %}


{% block content %}
    <div class="card card-primary">
        <div class="card-header">
            <h3 class="card-title">[{{ log_obj.log_sub_it.it_name }}] 的记录报告</h3>

            <div class="card-tools">
                <button type="button" class="btn btn-tool" data-card-widget="collapse"><i class="fas fa-minus"></i>
                </button>
                <button type="button" class="btn btn-tool" data-card-widget="remove"><i class="fas fa-times"></i>
                </button>
            </div>
        </div>
        <div class="card-body">
            <div class="chart">
                <div style="min-height: 250px; height: 100%; max-height: 850px; max-width: 100%;">
                    {{ log_obj.log_report | safe }}
                </div>
            </div>
            <div>
                <form action="" method="post">
                    {% csrf_token %}
                    <input type="text" name="report_pk" value="{{ log_obj.pk }}" hidden>
                    <a href="{% url 'logs_list' %}" class="btn btn-default">返回上一页</a>
                    <input type="submit" class="btn btn-success" value="下载报告">
                </form>
            </div>
        </div>
        <!-- /.card-body -->
    </div>
{% endblock %}
View Code

 

logs_list.html:

{% extends 'base.html' %}
​
​
​
{% block content %}
​
​
    <table class="table table-striped table-hover table-bordered">
        <thead>
        <tr>
            <th>序号</th>
            <th>所属项目</th>
            <th>创建时间</th>
            <th>执行用例总数</th>
            <th>用例失败数</th>
            <th>用例报错数</th>
            <th>用例通过数</th>
            <th>通过率</th>
            <th>用例报告</th>
        </tr>
        </thead>
        <tbody>
        {% for foo in logs_obj %}
            <tr>
                <td>{{ forloop.counter }}</td>
                <td>{{ foo.log_sub_it.it_name }}</td>
                <td>{{ foo.log_run_time }}</td>
                <td>{{ foo.log_run_count }}</td>
                <td>{{ foo.log_failed_count }}</td>
                <td>{{ foo.log_errors_count }}</td>
                <td>{{ foo.log_pass_count }}</td>
                <td>{{ foo.pass_rate }}</td>
                <td><a href="{% url 'preview' foo.pk %}">预览</a></td>
            </tr>
        {% endfor %}
​
        </tbody>
    </table>
​
{% endblock %}
 
View Code

 

可视化

三种方法:

  • hightcharts

  • echarts

前端从后端获取必要的数据,进行渲染展示

  • pyecharts: 百度开源的,后台自己处理数据,自己生成html页面 和前端没关系

pyecharts的使用

pip install wxpy  # 备用下载地址:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple wxpy
pip install pyecharts==0.5.11       # 备用下载地址:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pyecharts==0.5.11
pip install pyecharts_snapshot  # 备用下载地址:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pyecharts_snapshot

 

示例(朋友圈的性别比例):


import wxpy
import webbrowser
from pyecharts import Pie
# 1. 登录
bot = wxpy.Bot()         # cache_path=True  可以不填,但每次都要重新扫描
# 2. 获取所有的朋友对象,放到列表中
friends = bot.friends()
attr = ['男朋友', '女朋友', '性别不详']
value = [0, 0, 0]
for friend in friends:
    print(friend.name, friend.sex)
    if friend.sex == 1:     # 1代表男性
        value[0] += 1
    elif friend.sex == 2:   # 2代表女性
        value[1] += 1
    else:   # 未指定性别的
        value[2] += 1
# 3. 处理为图像
pie = Pie('%s 朋友圈性别比例图' % bot.self.name)
pie.add('', attr, value, is_label_show=True)  # 图表名称str,属性名称list,属性所对应的值list,is_label_show是否显示标签
pie.render('sex.html')  # 生成html页面
# 4. 打开浏览器展示
webbrowser.open('sex.html')
View Code

 

 

示例参考: https://www.cnblogs.com/Neeo/articles/10454764.html https://www.cnblogs.com/Neeo/articles/10454677.html

echarts的使用(此项目使用)

参考: https://echarts.apache.org/zh/index.html

找到echarts: https://www.bootcdn.cn/echarts/

https://cdn.bootcdn.net/ajax/libs/echarts/5.4.0/echarts.esm.min.js

将文件内容复制到项目中static目录下 模板就可以引用了

使用:

  1. 绑定一个标签,生成一个echarts对象

  2. 配置数据和参数

  3. setoption生成图表(通过ajax请求后台的数据并展示在图表)

urls.py:

from django.contrib import admin
from django.urls import path, re_path
from app01 import views
​
urlpatterns = [
...........
​
    # log日志
    path('logs_list/', views.logs_list, name='logs_list'),
    path('^show_tab/', views.show_tab, name='show_tab'),
​
]
View Code

 

views.py:

from utils.ShowTabHandler import ShowTabOpt
​
..........
​
def show_tab(request):
    """ 可视化 """
    if request.is_ajax():
        tab_obj = ShowTabOpt()
        data_dict = {}
        data_dict.update(tab_obj.pie())
        data_dict.update(tab_obj.line_simple())
        # print(data_dict)
        return JsonResponse(data_dict)
    else:
        return render(request, 'show_tab.html')
View Code

 

base.html:

......
​
<nav class="mt-2">
    <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu"
        data-accordion="false">
        <li class="nav-item">
            <a href="{% url 'index' %}" class="nav-link">
                <i class="nav-icon fas fa-th"></i>
                <p>
                    项目列表
                    <span class="right badge badge-danger">New</span>
                </p>
            </a>
        </li>
        <li class="nav-item">
            <a href="{% url 'logs_list' %}" class="nav-link">
                <i class="nav-icon fas fa-th"></i>
                <p>
                    log日志
                    <span class="right badge badge-danger">New</span>
                </p>
            </a>
        </li>
        <li class="nav-item">
            <a href="{% url 'show_tab' %}" class="nav-link">
                <i class="nav-icon fas fa-th"></i>
                <p>
                    可视图表
                    <span class="right badge badge-danger">New</span>
                </p>
            </a>
        </li>
    </ul>
</nav>
......
View Code

 

show_tab.html:

{% extends 'base.html' %}
{% load static %}
​
{% block content %}
    <div class="card card-primary">
        <div class="card-header">
            <h3 class="card-title">最近一年用例情况</h3><div class="card-tools">
                <button type="button" class="btn btn-tool" data-card-widget="collapse"><i class="fas fa-minus"></i>
                </button>
                <button type="button" class="btn btn-tool" data-card-widget="remove"><i class="fas fa-times"></i>
                </button>
            </div>
        </div>
        <div class="card-body">
            <div class="chart">
                <div id="LineSimple"
                     style="min-height: 250px; height: 400px; max-height: 450px; max-width: 100%;"></div>
            </div>
        </div>
        <!-- /.card-body -->
    </div>
​
​
    <div class="row">
        <div class="col-md-6">
            <div class="card card-primary">
                <div class="card-header">
                    <h3 class="card-title">用例执行情况统计</h3><div class="card-tools">
                        <button type="button" class="btn btn-tool" data-card-widget="collapse"><i
                                class="fas fa-minus"></i>
                        </button>
                        <button type="button" class="btn btn-tool" data-card-widget="remove"><i
                                class="fas fa-times"></i>
                        </button>
                    </div>
                </div><div class="card-body">
                    <div class="chart">
                        <div id="ExecuteChart"
                             style="min-height: 250px; height: 400px; max-height: 450px; max-width: 100%;"></div>
                    </div></div>
                <!-- /.card-body -->
            </div>
        </div>
        <div class="col-md-6">
            <div class="card card-primary">
                <div class="card-header">
                    <h3 class="card-title">用例通过情况统计</h3><div class="card-tools">
                        <button type="button" class="btn btn-tool" data-card-widget="collapse"><i
                                class="fas fa-minus"></i>
                        </button>
                        <button type="button" class="btn btn-tool" data-card-widget="remove"><i
                                class="fas fa-times"></i>
                        </button>
                    </div>
                </div><div class="card-body">
                    <div class="chart">
                        <div id="PassChart"
                             style="min-height: 250px; height: 400px; max-height: 450px; max-width: 100%;"></div>
                    </div></div>
                <!-- /.card-body -->
            </div>
        </div>
    </div>
​
​
​
    {% csrf_token %}
{% endblock %}
​
{% block js %}
​
    <script src="{% static 'echarts.min.js' %}"></script>
    <script>// 用例通过/未通过的饼图
        function PassPie(title, data) {
            // 1. 实例化 echarts 对象
            PieObj = echarts.init(document.getElementById("PassChart"));
            // 2, 配置参数
            PieObjOption = {
                title: {
                    // text: '某站点用户访问来源',
                    // subtext: '纯属虚构',
                    left: 'center'
                },
                tooltip: {
                    trigger: 'item',
                    formatter: '{a} <br/>{b} : {c} ({d}%)'
                },
                legend: {
                    orient: 'vertical',
                    left: 'left',
                    data: title
                },
                series: [
                    {
                        name: '用例',
                        type: 'pie',
                        radius: '55%',
                        center: ['50%', '60%'],
                        data: data,
                        emphasis: {
                            itemStyle: {
                                shadowBlur: 10,
                                shadowOffsetX: 0,
                                shadowColor: 'rgba(0, 0, 0, 0.5)'
                            }
                        }
                    }
                ]
            };
            // 3. setoption
            PieObj.setOption(PieObjOption);
        }
​
        // 用例执行/未执行的饼图
        function ExecutePie(title, data) {
            // 1. 实例化 echarts 对象
            PieObj = echarts.init(document.getElementById("ExecuteChart"));
            // 2, 配置参数
            PieObjOption = {
                title: {
                    // text: '某站点用户访问来源',
                    // subtext: '纯属虚构',
                    left: 'center'
                },
                tooltip: {
                    trigger: 'item',
                    formatter: '{a} <br/>{b} : {c} ({d}%)'
                },
                legend: {
                    orient: 'vertical',
                    left: 'left',
                    data: title
                },
                series: [
                    {
                        name: '用例',
                        type: 'pie',
                        radius: '55%',
                        center: ['50%', '60%'],
                        data: data,
                        emphasis: {
                            itemStyle: {
                                shadowBlur: 10,
                                shadowOffsetX: 0,
                                shadowColor: 'rgba(0, 0, 0, 0.5)'
                            }
                        }
                    }
                ]
            };
            // 3. setoption
            PieObj.setOption(PieObjOption);
        }
​
        // 近一年来,每月的用例数量走势图,折线图
        function LineSimple(title, data) {
            // 1. 实例化 echarts 对象
            lineSimple = echarts.init(document.getElementById("LineSimple"));
            // 2. 配置数据和参数
​
            /*
            lineSimpleOption = {
                xAxis: {
                    type: 'category',
                    data: title
                },
                yAxis: {
                    type: 'value'
                },
                series: [{
                    data: data,
                    type: 'line'
                }]
            };
            */
            lineSimpleOption = {
                xAxis: {
                    type: 'category',
                    boundaryGap: false,
                    data: title
                },
                yAxis: {
                    type: 'value'
                },
                series: [{
                    data: data,
                    type: 'line',
                    areaStyle: {}
                }]
            };
            // 3. 展示图标
            lineSimple.setOption(lineSimpleOption);
​
        }
​
        function init() {
            $.ajax({
                "url": "{% url 'show_tab' %}",
                "type": "POST",
                "data": {"csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val()},
                success: function (data) {
                    console.log(data);
                    // 用例通过/未通过的饼图
                    PassPie(data['pass_pie']['title'], data['pass_pie']['data']);
                    // 用例执行/未执行的饼图
                    ExecutePie(data['execute_pie']['title'], data['execute_pie']['data']);
                    // 折线图
                    LineSimple(data['line_simple']['title'], data['line_simple']['data']);
​
​
                }
            })
        }
​
        init()
​
    </script>
​
​
​
{% endblock %}
View Code

 

showtabhandler.py:


import os
import django
import datetime
from datetime import timedelta
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dengxin.settings")
django.setup()
​
​
from app01 import models
​
class ShowTabOpt(object):
    """ 处理图标所需要的数据 """def pie(self):
        """ 处理饼图 """
        data = {
            "pass_pie": {
                "title": ["通过", "失败"],
                "data": [
                    {"value": 0, "name": "通过"},
                    {"value": 0, "name": "失败"},
                ]
            },
            "execute_pie": {
                "title": ["已执行", "未执行"],
                "data": [
                    {"value": 0, "name": "已执行"},
                    {"value": 0, "name": "未执行"},
                ]
            }
        }
​
​
        # 数据库取数据
# 法1
        # models.Api.objects.filter(api_pass_status=1).count()
        # models.Api.objects.filter(api_pass_status=0).count()
        # models.Api.objects.filter(api_run_status=1).count()
        # models.Api.objects.filter(api_run_status=0).count()
# 法2 通过 it 表查
​
        it_obj = models.It.objects.all()
        for item in it_obj:
            # 已通过
            data["pass_pie"]['data'][0]['value'] += item.api_set.filter(api_pass_status=1).count()
            data["pass_pie"]['data'][1]['value'] += item.api_set.filter(api_pass_status=0).count()
            data["execute_pie"]['data'][0]['value'] += item.api_set.filter(api_run_status=1).count()
            data["execute_pie"]['data'][1]['value'] += item.api_set.filter(api_run_status=0).count()
​
​
        print(data)
​
        return data
​
​
    def line_simple(self):
        """ 折线图
        近一年,统计每个月的用例数据走势图(19年5月11号~20年5月11号) 根据 it_start_time 进行过滤
        """
        data_dict = {
                    "line_simple": {
                        "title": [],
                        "data": []
                    }
                }
​
        end_time = datetime.date.today()
        # 思考,如何获取去年的今天
        start_time = end_time - timedelta(days=365)
        # print(end_time, start_time)
        it_obj = models.It.objects.filter(it_start_time__range=(start_time, end_time))
        # print(it_obj)
        d = {}
        for item in it_obj:
            m = item.it_start_time.strftime("%Y-%m")
            if d.get(m, None): # 月份存在,加value值即可
                d[m] += item.api_set.count()
            else:
                d[m] = item.api_set.count()
​
        print(d.items())
        # 问题,如何根据字段key排序
        new_d = sorted(d.items(), key=lambda x: x[0])
        print(new_d)
        for i in new_d:
            data_dict['line_simple']['title'].append(i[0])
            data_dict['line_simple']['data'].append(i[1])
        print(data_dict)
​
        return data_dict
if __name__ == '__main__':
    ShowTabOpt().line_simple()
View Code

 

 

定时任务

常见用于定时任务的:

 

APScheduler(此项目用)

https://www.cnblogs.com/Neeo/p/10435059.html

pip install -i https://pypi.doubanio.com/simple/ apscheduler

 

使用示例:

指定时间执行一次(2019-2.25-19:5:6)

import datetime
from apscheduler.schedulers.blocking import BlockingScheduler
def job2(text):
    print('job2', datetime.datetime.now(), text)
# 新建一个调度器
scheduler = BlockingScheduler()
​
# 新建任务
scheduler.add_job(job2, 'date', run_date=datetime.datetime(2019, 2, 25, 19, 5, 6), args=['text'], id='job2')
# 运行任务
scheduler.start()

 

间隔每5秒执行一次:

import datetime
from apscheduler.schedulers.blocking import BlockingScheduler

def job1():
    print('job1', datetime.datetime.now())
scheduler = BlockingScheduler()
scheduler.add_job(job1, 'interval', seconds=5, id='job1')  # 每隔5秒执行一次
scheduler.start()

 

每天指定时间执行一次

# ----------------- 每天指定时间 执行一次 -----------------

from apscheduler.schedulers.blocking import BlockingScheduler  # 后台运行

sc = BlockingScheduler()
f = open('t1.txt', 'a', encoding='utf8')

@sc.scheduled_job('cron', day_of_week='*', hour=11, minute='56', second='2')  # 每天11点56分02秒执行一次
def check_db():
    print(111111111111)


if __name__ == '__main__':
    try:
        sc.start()
        f.write('定时任务成功执行')
    except Exception as e:
        sc.shutdown()
        f.write('定时任务执行失败')
    finally:
        f.close()

 

django发邮件

参考:https://www.cnblogs.com/Neeo/articles/11199085.html

简单配置发邮件

settings.py配置:

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = True  # 是否使用TLS安全传输协议(用于在两个通信应用程序之间提供保密性和数据完整性。)
EMAIL_USE_SSL = False  # 是否使用SSL加密,qq企业邮箱要求使用
EMAIL_HOST = 'smtp.163.com'  # 发送邮件的邮箱 的 SMTP服务器,这里用了163邮箱
EMAIL_PORT = 25  # 发件箱的SMTP服务器端口

# 上面配置可以不动,下面配置可以修改
EMAIL_HOST_USER = '邮箱@163.com'  # 发送邮件的邮箱地址
EMAIL_HOST_PASSWORD = '你的授权码'  # 发送邮件的邮箱密码(这里使用的是授权码)
EMAIL_TO_USER_LIST = ['接收人@qq.com']   # 此字段是可选的,用来配置收件人列表
View Code

 

views.py:

from django.http import HttpResponse
from dengxin import settings
from django.core.mail import send_mail

def send_email(request):
    send_mail(
        subject='这里是邮件标题',
        message='这里是邮件内容',
        from_email='tingyuweilou@163.com',
        recipient_list=settings.EMAIL_TO_USER_LIST,
        fail_silently=False
    )
    return HttpResponse('OK')
View Code

 

发送待附件的示例

views.py中:

from django.http import HttpResponse
from dengxin import settings
from django.core.mail import send_mail, EmailMessage
​
def send_email(request):
​
    # 发送带附件的邮件
    msg = EmailMessage(
        subject='这是带附件的邮件标题',
        body='这是带附件的邮件内容',
        from_email=settings.EMAIL_HOST_USER,  # 也可以从settings中获取
        to=settings.EMAIL_TO_USER_LIST
    )
    msg.attach_file(r'D:\video\s28-testing-day16-接口自动化平台-实现-3\note\dengxin\sex.html')
    msg.send(fail_silently=False)
    return HttpResponse('OK')
​
View Code

 

可以把发邮件功能和定时任务结合起来。

 

文件上传

有两种常用的文件上传方式:

  • ajax上传(此项目用这种)

  • form表单上传

参考:https://www.cnblogs.com/Neeo/articles/11021972.html

django事务:(transtation)

https://www.cnblogs.com/thomson-fred/p/10198528.html

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'^upload/(?P<pk>\d+)$', views.upload, name='upload'),  # 文件上传
.........
​
]
View Code

 

views.py:

import xlrd
# django事务
from django.db import transaction
​
​
def upload(request, pk):
    """ 文件上传 """
    if request.is_ajax():
        # print(request.POST)
        # print(request.FILES)
        try:
            with transaction.atomic():
                # 获取请求的上传文件
                file_obj = request.FILES.get("f1")
                # 获取用例项目的id
                it_obj_pk = request.POST.get("it_obj_pk")
                print(it_obj_pk, file_obj)  # 2 接口测试示例-2.xlsx
                # 读文件 filename=None不需要文件名
                book_obj = xlrd.open_workbook(filename=None, file_contents=file_obj.read())
                print(book_obj)
                sheet = book_obj.sheet_by_index(0)
                title = sheet.row_values(0)
                print(sheet, title)
                data_list = [dict(zip(title, sheet.row_values(item))) for item in range(1, sheet.nrows)]
                print(data_list)
                """
                [
                    {
                        'case_num': 'neeo_001',
                        'title': '下单接口',
                        'desc': 'neeo项目的下单接口',
                        'url': 'http://www.neeo.cc:6002/pinter/com/buy',
                        'method': 'post',
                        'params': '',
                        'data': '{"param":{"skuId":123,"num":10}}',
                        'json': '',
                        'cookies': '',
                        'headers': '',
                        'except': '{"code": "0", "message": "success"}'
                    }
                ]
                """# 参考上面的数据
                # 循环列表往数据库里写数据
                for item in data_list:
                    models.Api.objects.create(
                        api_sub_it_id=it_obj_pk,
                        api_name=item['title'],
                        api_desc=item['desc'],
                        api_url=item['url'],
                        api_method=item['method'],
                        api_params=item['params'],
                        api_data=item['data']
                    )
            return JsonResponse({"status": 200, 'path': '/list_api/{}'.format(pk)})
        except Exception as e:
            # ajax的报错处理和form表单的不一样
            return JsonResponse({
                "status": 500,
                'path': '/list_api/{}'.format(pk),
                "it_obj_pk": pk,
                "errors": "这里只能上传 [xls] or [xlsx] 类型的表格,并且表格的字段要符合要求, 错误详情:{}".format(e)
            })
    else:
        return render(request, 'upload.html', {"it_obj_pk": pk})
View Code

 

注意:xlrd无法识别xlsx文件 xlrd最新版本不支持xlsx文件,将版本回退即可。

pip show xlrd
​
pip install xlrd==1.2.0

 

upload.html:

此处用的ajax上传的文件

{% extends 'base.html' %}
​
​
{% block content %}
    <div class="alert alert-warning alert-dismissible" id="p1">
        <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
        <h5><i class="icon fas fa-exclamation-triangle"></i> 重要提示!</h5>
        <span id="error">
            这里只能上传Excel文件,
        Excel表格设计字段的时候,参考数据的字段设计,方便上传文件。
        </span></div>
    <div class="form-group">
        <label for="exampleInputFile">文件上传</label>
        <div class="input-group">
            <div class="custom-file">
                <input type="file" class="custom-file-input" id="ajaxFile">
                <label class="custom-file-label" for="exampleInputFile">选择文件</label>
            </div>
            <div class="input-group-append">
                <span class="input-group-text" id="ajaxBtn">上传</span>
            </div>
        </div>
    </div>
    <span>{{ errors }}</span>
    <div hidden id="it_obj_pk">{{ it_obj_pk }}</div>
    {% csrf_token %}
{% endblock %}
​
​
{% block js %}
    <script>
        // console.log(111111111, $("#it_obj_pk").text());
        $("#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中
​
            var it_obj_pk = $("#it_obj_pk").text();
            console.log(111111111, it_obj_pk);
            formData.append('it_obj_pk', it_obj_pk);
            $.ajax({
                url: "/upload/" + it_obj_pk,
                type: "POST",
                data: formData,
                processData: false,  //
                contentType: false,
                success: function (dataMsg) {
                    console.log(dataMsg);
                    // 处理ajax请求的后端报错
                    if (dataMsg['status'] == 500) {
                        $("#p1").attr("class", 'alert alert-danger alert-dismissible');
                        $("#error").text(dataMsg['errors'])
                    } else {
                        window.location.href = dataMsg['path']
                    }
​
                }
            })
        })
    </script>
{% endblock %}
View Code

 

index.html:

增加批量导入的入口

......
<td>
    <a href="{% url 'delete_it' foo.pk %}" class="btn btn-danger btn-sm">删除</a>
    <a href="{% url 'edit_it' foo.pk %}" class="btn btn-info btn-sm">编辑</a>
    <a href="{% url 'add_api' foo.pk %}" class="btn btn-warning btn-sm">添加用例</a>
    <a href="{% url 'upload' foo.pk %}" class="btn btn-default btn-sm">批量导入</a>
    <a href="{% url 'list_api' foo.pk %}" class="btn btn-success btn-sm">查看用例列表</a>
</td>
......
 
View Code

 

周末作业

日志表相关的作业

把预览完成

  • 点击预览,跳转到一个新的页面中,在新的页面中,有返回上一页和下载按钮

    • 点击返回上一页,跳转到logs_list页面

    • 点击下载,就把当前的报告下载到本地

 

 

 

 

 

urls.py:

urlpatterns = [
......
    # 执行用例
    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'),
    path('show_tab/', views.show_tab, name='show_tab'),
    re_path(r'^preview/(?P<pk>\d+)$', views.preview, name='preview'),
​
]
View Code

 

 

views.py:

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})
​
​
def preview(request, pk):
    """ 测试报告预览 , pk:logs记录的pk"""
    if request.method == "POST":
        report_pk = request.POST.get("report_pk")
        log_obj = models.Logs.objects.filter(pk=report_pk).first()
        # 下载
        response = FileResponse(log_obj.log_report)
        response['Content-Type'] = 'application/octet-stream'
        response['Content-Disposition'] = 'attachment;filename="{}.{}"'.format(escape_uri_path(log_obj.log_sub_it.it_name),
                                                                               'html')
        return response
    log_obj = models.Logs.objects.filter(pk=pk).first()
    print(log_obj)
    return render(request, 'preview.html', {"log_obj": log_obj})
View Code

 

priview.html:

{% extends 'base.html' %}
​
​
{% block content %}
​
​
    <div class="card card-primary">
        <div class="card-header">
            <h3 class="card-title">[{{ log_obj.log_sub_it.it_name }}] 的记录报告</h3><div class="card-tools">
                <button type="button" class="btn btn-tool" data-card-widget="collapse"><i class="fas fa-minus"></i>
                </button>
                <button type="button" class="btn btn-tool" data-card-widget="remove"><i class="fas fa-times"></i>
                </button>
            </div>
        </div>
        <div class="card-body"><div class="chart">
                <div style="min-height: 250px; height: 100%; max-height: 850px; max-width: 100%;">
                    {{ log_obj.log_report | safe }}
                </div>
            </div>
            <div>
                <form action="" method="post">
                    {% csrf_token %}
                    <input type="text" name="report_pk" value="{{ log_obj.pk }}" hidden>
                    <a href="{% url 'logs_list' %}" class="btn btn-default">返回上一页</a>
                    <input type="submit" class="btn btn-success" value="下载报告">
                </form>
            </div>
        </div>
        <!-- /.card-body -->
    </div>
{% endblock %}
View Code

 

logs_list.html

<td><a href="{% url 'preview' foo.pk %}">预览</a></td>
View Code

 

可视化

https://echarts.apache.org/examples/zh/index.html

使用echarts展示三个图:

  • 用例通过/失败(饼图)

  • 用例执行/未执行(饼图)

  • 展示最近一年的每个月的项目创建数量(折线图),注意,提前准备数据

 

 

show_tab.html:

{% extends 'base.html' %}
{% load static %}
​
{% block content %}
    <div class="card card-primary">
        <div class="card-header">
            <h3 class="card-title">最近一年用例情况</h3><div class="card-tools">
                <button type="button" class="btn btn-tool" data-card-widget="collapse"><i class="fas fa-minus"></i>
                </button>
                <button type="button" class="btn btn-tool" data-card-widget="remove"><i class="fas fa-times"></i>
                </button>
            </div>
        </div>
        <div class="card-body">
            <div class="chart">
                <div id="LineSimple"
                     style="min-height: 250px; height: 400px; max-height: 450px; max-width: 100%;"></div>
            </div>
        </div>
        <!-- /.card-body -->
    </div>
​
​
    <div class="row">
        <div class="col-md-6">
            <div class="card card-primary">
                <div class="card-header">
                    <h3 class="card-title">用例执行情况统计</h3><div class="card-tools">
                        <button type="button" class="btn btn-tool" data-card-widget="collapse"><i
                                class="fas fa-minus"></i>
                        </button>
                        <button type="button" class="btn btn-tool" data-card-widget="remove"><i
                                class="fas fa-times"></i>
                        </button>
                    </div>
                </div><div class="card-body">
                    <div class="chart">
                        <div id="ExecuteChart"
                             style="min-height: 250px; height: 400px; max-height: 450px; max-width: 100%;"></div>
                    </div></div>
                <!-- /.card-body -->
            </div>
        </div>
        <div class="col-md-6">
            <div class="card card-primary">
                <div class="card-header">
                    <h3 class="card-title">用例通过情况统计</h3><div class="card-tools">
                        <button type="button" class="btn btn-tool" data-card-widget="collapse"><i
                                class="fas fa-minus"></i>
                        </button>
                        <button type="button" class="btn btn-tool" data-card-widget="remove"><i
                                class="fas fa-times"></i>
                        </button>
                    </div>
                </div><div class="card-body">
                    <div class="chart">
                        <div id="PassChart"
                             style="min-height: 250px; height: 400px; max-height: 450px; max-width: 100%;"></div>
                    </div></div>
                <!-- /.card-body -->
            </div>
        </div>
    </div>
​
​
​
    {% csrf_token %}
{% endblock %}
​
{% block js %}
​
    <script src="{% static 'echarts.min.js' %}"></script>
    <script>// 用例通过/未通过的饼图
        function PassPie(title, data) {
            // 1. 实例化 echarts 对象
            PieObj = echarts.init(document.getElementById("PassChart"));
            // 2, 配置参数
            PieObjOption = {
                title: {
                    // text: '某站点用户访问来源',
                    // subtext: '纯属虚构',
                    left: 'center'
                },
                tooltip: {
                    trigger: 'item',
                    formatter: '{a} <br/>{b} : {c} ({d}%)'
                },
                legend: {
                    orient: 'vertical',
                    left: 'left',
                    data: title
                },
                series: [
                    {
                        name: '用例',
                        type: 'pie',
                        radius: '55%',
                        center: ['50%', '60%'],
                        data: data,
                        emphasis: {
                            itemStyle: {
                                shadowBlur: 10,
                                shadowOffsetX: 0,
                                shadowColor: 'rgba(0, 0, 0, 0.5)'
                            }
                        }
                    }
                ]
            };
            // 3. setoption
            PieObj.setOption(PieObjOption);
        }
​
        // 用例执行/未执行的饼图
        function ExecutePie(title, data) {
            // 1. 实例化 echarts 对象
            PieObj = echarts.init(document.getElementById("ExecuteChart"));
            // 2, 配置参数
            PieObjOption = {
                title: {
                    // text: '某站点用户访问来源',
                    // subtext: '纯属虚构',
                    left: 'center'
                },
                tooltip: {
                    trigger: 'item',
                    formatter: '{a} <br/>{b} : {c} ({d}%)'
                },
                legend: {
                    orient: 'vertical',
                    left: 'left',
                    data: title
                },
                series: [
                    {
                        name: '用例',
                        type: 'pie',
                        radius: '55%',
                        center: ['50%', '60%'],
                        data: data,
                        emphasis: {
                            itemStyle: {
                                shadowBlur: 10,
                                shadowOffsetX: 0,
                                shadowColor: 'rgba(0, 0, 0, 0.5)'
                            }
                        }
                    }
                ]
            };
            // 3. setoption
            PieObj.setOption(PieObjOption);
        }
​
        // 近一年来,每月的用例数量走势图,折线图
        function LineSimple(title, data) {
            // 1. 实例化 echarts 对象
            lineSimple = echarts.init(document.getElementById("LineSimple"));
            // 2. 配置数据和参数
​
            /*
            lineSimpleOption = {
                xAxis: {
                    type: 'category',
                    data: title
                },
                yAxis: {
                    type: 'value'
                },
                series: [{
                    data: data,
                    type: 'line'
                }]
            };
            */
            lineSimpleOption = {
                xAxis: {
                    type: 'category',
                    boundaryGap: false,
                    data: title
                },
                yAxis: {
                    type: 'value'
                },
                series: [{
                    data: data,
                    type: 'line',
                    areaStyle: {}
                }]
            };
            // 3. 展示图标
            lineSimple.setOption(lineSimpleOption);
​
        }
​
        function init() {
            $.ajax({
                "url": "{% url 'show_tab' %}",
                "type": "POST",
                "data": {"csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val()},
                success: function (data) {
                    console.log(data);
                    // 用例通过/未通过的饼图
                    PassPie(data['pass_pie']['title'], data['pass_pie']['data']);
                    // 用例执行/未执行的饼图
                    ExecutePie(data['execute_pie']['title'], data['execute_pie']['data']);
                    // 折线图
                    LineSimple(data['line_simple']['title'], data['line_simple']['data']);
​
​
                }
            })
        }
​
        init()
​
    </script>
​
​
​
{% endblock %}
View Code

 

views.py:

.......
def show_tab(request):
    """ 可视化 """
    if request.is_ajax():
        tab_obj = ShowTabOpt()
        data_dict = {}
        data_dict.update(tab_obj.pie())
        data_dict.update(tab_obj.line_simple())
        # print(data_dict)
        return JsonResponse(data_dict)
    else:
        return render(request, 'show_tab.html')
........
View Code

 

showtabhandler.py:

# -*- coding: utf-8 -*-
# @Time    : 2020/5/11 15:24
# @Author  : 张开
# File      : ShowTabHandler.py
​
​
"""  处理可视化相关 """import os
import django
import datetime
from datetime import timedelta
​
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "adminzdh.settings")
django.setup()
​
from app01 import models
​
​
class ShowTabOpt(object):
    """ 处理图标所需要的数据 """def pie(self):
        """ 处理饼图 """
        data = {
            "pass_pie": {
                "title": ["通过", "失败"],
                "data": [
                    {"value": 0, "name": "通过"},
                    {"value": 0, "name": "失败"},
                ]
            },
            "execute_pie": {
                "title": ["已执行", "未执行"],
                "data": [
                    {"value": 0, "name": "已执行"},
                    {"value": 0, "name": "未执行"},
                ]
            }
        }
​
        # 数据库取数据
# 法1
        # models.Api.objects.filter(api_pass_status=1).count()
        # models.Api.objects.filter(api_pass_status=0).count()
        # models.Api.objects.filter(api_run_status=1).count()
        # models.Api.objects.filter(api_run_status=0).count()
# 法2 通过 it 表查
​
        it_obj = models.It.objects.all()
        for item in it_obj:
            # 已通过 数量
            data["pass_pie"]['data'][0]['value'] += item.api_set.filter(api_pass_status=1).count()
            # 未通过
            data["pass_pie"]['data'][1]['value'] += item.api_set.filter(api_pass_status=0).count()
            # 已执行
            data["execute_pie"]['data'][0]['value'] += item.api_set.filter(api_run_status=1).count()
            # 未执行
            data["execute_pie"]['data'][1]['value'] += item.api_set.filter(api_run_status=0).count()
​
        print(data)
​
        return data
​
    def line_simple(self):
        """ 折线图
        近一年,统计每个月的用例数据走势图(19年5月11号~20年5月11号) 根据 it_start_time 进行过滤
        """
        data_dict = {
            "line_simple": {
                "title": [],
                "data": []
            }
        }
​
        end_time = datetime.date.today()
        # 思考,如何获取去年的今天
        start_time = end_time - timedelta(days=365)
        # print(end_time, start_time)
        it_obj = models.It.objects.filter(it_start_time__range=(start_time, end_time))
        # print(it_obj)
        d = {}
        for item in it_obj:
            m = item.it_start_time.strftime("%Y-%m")
            if d.get(m, None):  # 月份存在,加value值即可
                d[m] += item.api_set.count()
            else:
                d[m] = item.api_set.count()
​
        print(d.items())
        # 问题,如何根据字段key排序
        new_d = sorted(d.items(), key=lambda x: x[0])
        print(new_d)
        for i in new_d:
            data_dict['line_simple']['title'].append(i[0])
            data_dict['line_simple']['data'].append(i[1])
        print(data_dict)
​
        return data_dict
​
​
if __name__ == '__main__':
    ShowTabOpt().line_simple()
 
View Code

 

定时任务

每天凌晨1点30分50秒,检查it表,it_end_time是当前日期的接口项目,提取出来,获取该项目下所有的用例,批量执行一次,并生成log日志。

扩展:使用django发邮件,附件是批量执行的报告

建议:

多线程执行该任务

定时任务:

  • 需求是:每天的凌晨1:30:20去检查it表,查看 it_end_time 是当天的,就把其关联的所有的用例执行一遍。

    • 问题:如何实现?思路是随着django环境的运行,启动一个线程,调用一个 job 来实现需求。

utils/CrontabHandler.py:

import os
import django
import datetime
from apscheduler.schedulers.blocking import BlockingScheduler
​
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "adminzdh.settings")
​
django.setup()
​
from app01 import models
from utils.RequestHandler import run_case
​
​
def job1():
    """ 定时任务的逻辑实现  """
    # 循环 it 表的 it_end_time 字段,判断该字段的值是否是当天,如果是当天的,就执行其内的所有用例
    it_obj = models.It.objects.all()
    today = datetime.date.today()
    print(it_obj)
    for item in it_obj:
        if item.it_end_time == today:
            print(item.api_set.all())
            run_case(item.api_set.all())
​
​
def foo():
    scheduler = BlockingScheduler()
    scheduler.add_job(job1, 'interval', seconds=10, id='job1')  # 每隔5秒执行一次
    scheduler.start()
​
​
if __name__ == '__main__':
    job1()
View Code

 

 

manage.py:

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
from threading import Thread
​
def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'adminzdh.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
​
    from utils.CrontabHandler import foo
    # 启一个线程
    t = Thread(target=foo)
    t.start()
​
    execute_from_command_line(sys.argv)
​
​
if __name__ == '__main__':
    main()
View Code

 

优化:

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 dengxin.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('values_changed', None), None, msg=self.msg)
        # value change
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
        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)
        self.update_log_status(result, f)
​
    def update_log_status(self, result, f):
        """ 更新log表 """
        # 写log表,通过多少,失败多少,共执行了多少用例
        models.Logs.objects.create(
            log_report=f.getvalue(),
            log_sub_it_id=self.case_obj.api_sub_it_id,
            log_pass_count=result.__dict__['success_count'],
            log_errors_count=result.__dict__['error_count'],
            log_failed_count=result.__dict__['failure_count'],
            log_run_count=result.__dict__['testsRun']
        )
​
    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(case_obj=i, suite_list=suite_list).handler()
    # 问题,批量执行时,都能单独的执行并且生成各自的测试报告,但如何生成多个用例的报告呢?并且将该报告写入到log表中
    # print(111111111, suite_list)
​
    RequestOperate(case_obj=i, suite_list=suite_list).create_m_report(suite_list)
View Code

 

posted @ 2022-12-01 14:54  贰号猿  阅读(66)  评论(0)    收藏  举报