Django 接口自动化测试平台

2022年01月08日 阅读数:214
这篇文章主要向大家介绍Django 接口自动化测试平台,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

本项目工程 github 地址:https://github.com/juno3550/InterfaceAutoTestPlatform

0. 引言

1. 登陆功能

2. 项目

3. 模块

4. 测试用例

5. 用例集合

6. 用例集合添加测试用例

7. 用例集合查看/删除测试用例

8. 测试用例执行

9. 用例执行结果展现

10. 测试集合执行

11. 用例集合执行结果展现

12. 用例集合历史执行结果统计

13. 用例集合单次执行结果统计

14. 模块测试结果统计

15. 项目测试结果统计

16. Celery 异步执行用例 

 

 

0. 引言

0.1 平台功能概述

本接口自动化测试平台的总体功能以下图所示:javascript

0.2 建立项目与应用

本项目环境以下:css

  • python 3.6.5
  • pip install django==2.2.4
  • pip install redis==2.10.6
  • pip install eventlet==0.25.2
  • pip install celery==3.1.26.post2

对于 Django 的基础使用教程,能够参考本博客的《Django》系列博文。html

1)建立本项目工程

# 方式一
django-admin startproject InterfaceAutoTest

# 方式二
python -m django startproject interface_test_platform

此时工程结构以下所示:前端

2)建立应用

在项目目录下,使用以下命令建立应用:html5

django-admin startapp interfacetestplatform

此时工程结构目录以下:java

3)注册应用

在项目目录 InterfaceAutoTest/settings.py 中,找到 INSTALLED_APPS 配置项,在列表末尾添加新建的应用名称“interfacetestplatform”:python

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'interfacetestplatform,'
]

新建的应用须要在 INSTALLED_APPS 配置项中注册,Django 会根据该配置项到应用下查找模板等信息。mysql

4)使用 Bootstrap

Bootstrap 是流行的 HTML、CSS 和 JS 框架,用于开发响应式布局、移动设备优先的 Web 项目。jquery

在项目目录下新建一个 static 目录,将解压后的如 bootstrap-3.3.7-dist 目录总体拷贝到 static 目录中,并将文件名改成 bootstrap。git

因为 bootstrap 依赖 jQuery,咱们须要提早下载并引入 jQuery。在 static 目录下,新建 css 和 js 目录,做为后面的样式文件和 js 文件的存放地,将咱们的 jQuery 文件拷贝到 static/js/ 目录下。

ECharts 是一个使用 JavaScript 实现的开源可视化库,在本项目中用于用例/集合的执行结果统计可视化。因而咱们引入 echarts 文件到 static/js/ 目录下。

此时 static 目录结构以下图所示:

在项目目录下的 settings.py 文件的末尾添加以下配置,用于指定静态文件的搜索目录:

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

经过 STATICFILES_DIRS 属性,能够指定静态资源的路径,此处配置的是项目根目录下的 static 文件夹下。

默认状况下,Django 只能识别项目应用目录下的 static 目录的静态资源,不会识别到项目目录下的 static目录,所以经过 STATICFILES_DIR 属性能够解决这个问题。

5)建立应用的模板目录 

在应用目录下新建一个 templates 目录,专门存放本应用全部的模板文件。

默认状况下,Django 只能识别项目应用目录下的 templates 目录的静态资源。本工程因仅涉及一个应用,所以选用此方案。

若应用较多,为了易于维护,可将各应用的模板文件进行统一处理,则在项目目录下新建 templates 目录,并在 settings.py 中新增配置:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

6)启动 Django 的 Web 服务

在项目目录下,运行以下命令启动端口号为 8000 的 Django 的 Web 服务: 

python manage.py runsever 8000  # 端口号可自定义,不写则默认8000

启动日志以下:

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
July 13, 2021 - 10:03:23
Django version 3.2.5, using settings 'InterfaceAutoTest.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

启动成功后,用浏览器访问 127.0.0.1:8000,能够看到 Django 默认的页面,以下所示即表明服务启动成功:

7)配置 Mysql 数据库

修改项目目录下的 settings.py 文件,将 DATABASES 配置项改成以下配置:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'interface_platform',  # 已存在的库名
         'USER': 'root',  # 数据库帐号
         'PASSWORD': '123',  # 数据库密码
         'HOST': '127.0.0.1',  # 数据库IP
        'PORT': '3306',  # 数据库端口
    }
}

若使用的是 Python 3 的 pymysql,则须要在项目的 __init__.py 文件中添加以下内容,避免报错:

import pymysql
pymysql.version_info = (1, 4, 13, "final", 0)  # 指定版本。在出现“mysqlclient 1.4.0 or newer is required; you have 0.9.3.”报错时加上此行
pymysql.install_as_MySQLdb()

8)建立 Admin 站点超级用户

首先在项目目录下进行数据迁移,生成 auth_user 等基础表:

python manage.py migrate

在工程目录下,执行如下命令建立超级用户,用于登陆 Django 的 Admin 站点:

D:\InterfaceAutoTest>python manage.py createsuperuser
Username (leave blank to use 'administrator'): admin  # 用户名
Email address: admin@123.com  # 符合邮箱格式便可
Password:    # 密码
Password (again):    # 二次确认密码
The password is too similar to the username.
This password is too common.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

建立好 superuser 后,访问 127.0.0.1:8000/admin,用注册的 admin 帐户进行登陆(数据参考的就是 auth_user 表,而且必须是管理员用户才能进入)。

配置 admin 语言和时区

登陆 admin 页面,能够看到页面的标题、菜单显示的语言是英文,那么如何展现为中文呢?

在 settings.py 中,找到 MIDDLEWARE 属性,添加中间件。

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.locale.LocaleMiddleware', 
'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]

 

同时,在后续添加项目信息表数据时,能够发现数据的建立时间、更新时间字段的时间不是本地时间,而是 UTC 时间,那么怎么展现为国内本地时间呢?

也是 settings.py 中,修改 TIME_ZONE 和 USE_TZ 的配置项:

TIME_ZONE = 'Asia/Shanghai'

USE_TZ = False

这样配置后,admin 页面中数据表的时间字段以及前端模板中的时间字段都是本地时区的时间。

  •  USE_TZ  False 时,TIME_ZONE值是 Django 存储全部日期时间的时区。
  •  USE_TZ  True 时,TIME_ZONE值是 Django 在模板中显示日期时间和解释表单中输入的日期时间的默认时区。简单起见,USE_TZ 直接设为 False

  

1. 登陆功能

预期效果以下:

1.1 定义路由

1) 路由定义规则说明

路由的基本信息包含路由地址和视图函数,路由地址即访问网址,视图函数即客户端在访问指定网址后,服务器端所对应的处理逻辑。

在应用目录(interfacetestplatform 目录)下新建 urls.py,将全部属于该应用的路由都写入该文件中,这样更容易管理和区分每一个应用的路由地址,而项目目录(InterfaceAutoTest)下的 urls.py 是将每一个应用的 urls.py 进行统一管理。

这种路由设计模式是 Django 经常使用的,其工做原理以下:

  1. 运行 InterfaceAutoTest 项目时,Django 从 interfacetestplatform (应用)目录的 urls.py 找到对应应用所定义的路由信息,生成完整的路由表。
  2. 当用户在浏览器上访问某个路由地址时,Django 服务端就会收到该用户的请求信息。Django 从当前请求信息获取路由地址,首先匹配项目目录下的 urls.py 的路由列表。转发到指定应用的 urls.py 的路由列表。
  3. 再执行应用下的路由信息所指向的视图函数,从而完成整个请求响应过程。

2)路由配置

配置项目 urls.py:InterfaceAutoTest/urls.py

from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path('admin/', admin.site.urls),  # 指向内置Admin后台系统的路由文件sites.py
    path('', include('interfacetestplatform.urls')),  # 指向interfacetestplatform应用的urls.py
    ]

因为默认地址分发给了 interfacetestplatform(应用)的 urls.py 进行处理,所以须要对 interfacetestplatform/urls.py 编写路由信息,代码以下:

from django.urls import path
from . import views


urlpatterns = [
    path('', views.index),
    path('login/', views.login),
    path('logout/', views.logout),
]

如路由信息 path('', views.index) 的 views.index 是指专门处理网站默认页的用户请求和响应过程的视图函数名称 index,其余路由规则原理同样。

1.2 定义视图函数

1)定义 Form 表单类

在定义视图函数以前,咱们先定义 Django 提供的表单模型类,来代替原生的前端 Form 表单。

在应用目录下,新建 form.py:

1 from django import forms
2 
3 
4 class UserForm(forms.Form):
5     username = forms.CharField(label="用户名", max_length=128, widget=forms.TextInput(attrs={'class': 'form-control'}))
6     password = forms.CharField(label="密码", max_length=256, widget=forms.PasswordInput(attrs={'class': 'form-control'}))

2)定义视图函数

在应用的 interfacepestplatform/views.py (视图模块)中添加以下代码:

 1 from django.shortcuts import render, redirect
 2 from django.contrib import auth  # Django用户认证(Auth)组件通常用在用户的登陆注册上,用于判断当前的用户是否合法
 3 from django.contrib.auth.decorators import login_required
 4 from .form import UserForm
 5 import traceback
 6 
 7 
 8 # Create your views here.
 9 # 默认页的视图函数
10 @login_required
11 def index(request):
12     return render(request, 'index.html')
13 
14 
15 # 登陆页的视图函数
16 def login(request):
17     print("request.session.items(): {}".format(request.session.items()))  # 打印session信息
18     if request.session.get('is_login', None):
19         return redirect('/')
20     # 若是是表单提交行为,则进行登陆校验
21     if request.method == "POST":
22         login_form = UserForm(request.POST)
23         message = "请检查填写的内容!"
24         if login_form.is_valid():
25             username = login_form.cleaned_data['username']
26             password = login_form.cleaned_data['password']
27             try:
28                 # 使用django提供的身份验证功能
29                 user = auth.authenticate(username=username, password=password)  # 从auth_user表中匹配信息,匹配成功则返回用户对象;反之返回None
30                 if user is not None:
31                     print("用户【%s】登陆成功" % username)
32                     auth.login(request, user)
33                     request.session['is_login'] = True
34                     # 登陆成功,跳转主页
35                     return redirect('/')
36                 else:
37                     message = "用户名不存在或者密码不正确!"
38             except:
39                 traceback.print_exc()
40                 message = "登陆程序出现异常"
41         # 用户名或密码为空,返回登陆页和错误提示信息
42         else:
43             return render(request, 'login.html', locals())
44     # 不是表单提交,表明只是访问登陆页
45     else:
46         login_form = UserForm()
47         return render(request, 'login.html', locals())
48 
49 
50 # 注册页的视图函数
51 def register(request):
52     return render(request, 'register.html')
53 
54 
55 # 登出的视图函数:重定向至login视图函数
56 @login_required
57 def logout(request):
58     auth.logout(request)
59     request.session.flush()
60     return redirect("/login/")

render 方法

用来生成网页内容并返回给客户端,其两个必须参数表示:第一个参数是浏览器想服务器发送的请求对象;第二个参数是模板名称,用于生成网页内容。

auth 模块

auth 模块是 cookie 和 session 的升级版。

auth 模块是对登陆认证方法的一种封装,以前咱们获取用户输入的用户名及密码后须要本身从 user 表里查询有没有用户名和密码符合的对象,而有了 auth 模块以后就能够很轻松地去验证用户的登陆信息是否存在于数据库(auth_user 表)中。

除此以外,auth 还对 session 作了一些封装,方便咱们校验用户是否已登陆。

login 方法

对于非 POST 方法的请求(如 GET 请求),直接返回空的表单,让用户能够填入数据。

对于 POST 方法请求,接收表单数据,并验证:

  1. 使用表单类自带的 is_valid() 方法进一步完成数据验证工做;
  2. 验证成功后能够从表单对象的 cleand_data 数据字典中获取表单的具体值;
  3. 若是验证不经过,则返回一个包含先前数据的表单给前端页面,方便用户修改。也就是说,它会帮你保留先前填写的数据内容,而不是返回一个空表。
  4. 另外,这里使用了一个小技巧,python 内置了一个 locals() 函数,它返回当前全部的本地变量字典,咱们能够偷懒的将这做为 render 函数的数据字典参数值,就不用费劲去构造一个形如 {'message':message, 'login_form':login_form} 的字典了。这样作的好处是大大方便了咱们;但同时也可能往模板传入一些多余的变量数据,形成数据冗余下降效率。

@login_required

为函数增长装饰器 @login_required,这种方式能够实现未登陆禁止访问首页的功能。

此种方式须要在项目 settings.py 中添加以下配置:

LOGIN_URL = '/login/'

经过 LOGIN_URL 告诉 Django 在用户没登陆的状况下,重定向的登陆地址;若是没配置该属性,Django 会重定向到默认地址。 

若是不用装饰器方式,也能够在函数内部判断用户状态,并实现重定向。以下所示,效果是同样的:

def index(request):
    print("request.user.is_authenticated: {}".format(request.user.is_authenticated))
    if not request.user.is_authenticated:
        return redirect("/login/")
    return render(request,'index.html')

1.3 定义模板文件

1)定义 base 基础模板,供其余模板继承

新建 interfacetestplatform/templates/base.html,用做平台前端的基础模板,代码以下:

 1 <!DOCTYPE html>
 2 <html lang="zh-CN">
 3 {% load static %}
 4 <head>
 5     <meta charset="utf-8">
 6     <meta http-equiv="X-UA-Compatible" content="IE=edge">
 7     <meta name="viewport" content="width=device-width, initial-scale=1">
 8     <!-- 上述3个meta标签*必须*放在最前面,任何其余内容都*必须*跟随其后! -->
 9     <title>{% block title %}base{% endblock %}</title>
10 
11     <!-- Bootstrap -->
12     <link href="{% static 'bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
13 
14     <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
15     <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
16     <!--[if lt IE 9]>
17     <script src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
18     <script src="https://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
19     <![endif]-->
20     {% block css %}{% endblock %}
21 </head>
22 <body>
23 <nav class="navbar navbar-default">
24     <div class="container-fluid">
25         <!-- Brand and toggle get grouped for better mobile display -->
26         <div class="navbar-header">
27             <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#my-nav"
28                     aria-expanded="false">
29                 <span class="sr-only">切换导航条</span>
30                 <span class="icon-bar"></span>
31                 <span class="icon-bar"></span>
32                 <span class="icon-bar"></span>
33             </button>
34             <a class="navbar-brand" href="#">自动化测试平台</a>
35         </div>
36 
37         <div class="collapse navbar-collapse" id="my-nav">
38             <ul class="nav navbar-nav">
39                 <li class="active"><a href="/">主页</a></li>
40             </ul>
41             <ul class="nav navbar-nav navbar-right">
42                 {% if request.user.is_authenticated %}
43                 <li><a href="#">当前在线:{{ request.user.username }}</a></li>
44                 <li><a href="/logout">登出</a></li>
45                 {% else %}
46                 <li><a href="/login">登陆</a></li>
47 
48                 {% endif %}
49             </ul>
50         </div><!-- /.navbar-collapse -->
51     </div><!-- /.container-fluid -->
52 </nav>
53 
54 {% block content %}{% endblock %}
55 
56 
57 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
58 <script src="{% static 'js/jquery-3.4.1.js' %}"></script>
59 <!-- Include all compiled plugins (below), or include individual files as needed -->
60 <script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script>
61 </body>
62 </html>

在 base 模板中,经过 if 判断,当登陆成功时,显示当前用户名和登出按钮;未登陆时,则显示登陆按钮。

注意,request 这个变量是默认被传入模板中的,能够经过圆点的调用方式获取对象中的属性,如 {{ reques.user.username }} 表示当前登陆的用户名。

2)定义 login 登陆模板

首先在 static/css/ 目录中,新建一个 login.css 样式文件,配置一些简单样式:

 1 body {
 2   background-color: #eee;
 3 }
 4 .form-login {
 5   max-width: 330px;
 6   padding: 15px;
 7   margin: 0 auto;
 8 }
 9 .form-login .form-control {
10   position: relative;
11   height: auto;
12   -webkit-box-sizing: border-box;
13      -moz-box-sizing: border-box;
14           box-sizing: border-box;
15   padding: 10px;
16   font-size: 16px;
17 }
18 .form-login .form-control:focus {
19   z-index: 2;
20 }
21 .form-login input[type="text"] {
22   margin-bottom: -1px;
23   border-bottom-right-radius: 0;
24   border-bottom-left-radius: 0;
25 }
26 .form-login input[type="password"] {
27   margin-bottom: 10px;
28   border-top-left-radius: 0;
29   border-top-right-radius: 0;
30 }

而后新建 templates/login.html:

 1 {% extends 'base.html' %}
 2 {% load static %}
 3 {% block title %}登陆{% endblock %}
 4 {% block css %}
 5 <link rel="stylesheet" href="{% static 'css/login.css' %}">
 6 {% endblock %}
 7 
 8 {% block content %}
 9 <div class="container">
10     <div class="col-md-4 col-md-offset-4">
11         <form class='form-login' action="/login/" method="post">
12             {% csrf_token %}
13 
14             <!-- 若是有登陆信息,则提示 -->
15             {% if message %}
16                 <div class="alert alert-warning">{{ message }}</div>
17             {% endif %}
18 
19             <h2 class="text-center">欢迎登陆</h2>
20 
21             <div class="form-group">
22                 {{ login_form.username.label_tag }}
23                 {{ login_form.username}}
24             </div>
25             <div class="form-group">
26                 {{ login_form.password.label_tag }}
27                 {{ login_form.password }}
28             </div>
29 
30             <button type="reset" class="btn btn-default pull-left">重置</button>
31             <button type="submit" class="btn btn-primary pull-right">提交</button>
32         </form>
33     </div>
34 </div> <!-- /container -->
35 {% endblock %}

login 模板说明:

  • 经过 “{% extends 'base.html' %}” 继承了“base.html”模板的内容。
  • 经过 “{% block title %} 登陆 {% endblock %}” 设置了专门的 title。
  • 若页面提示 CSRF 验证失败,咱们须要在前端页面的 form 表单内添加一个 {% csrf_token %} 标签,CSRF(Cross-site request forgery)跨站请求伪造,是一种常见的网络攻击手段。为此,Django 自带了许多常见攻击手段的防护机制,CSRF 就是其中一种,还有防 XSS、SQL 注入等。
  • 经过 “{% block css %}”引入了针对性的 login.css 样式文件。
  • 主体内容定义在“{% block content %}”中。
  • 使用 {{ login_form.name_of_field }} 方式分别获取每个字段,而后分别进行渲染。

3)定义 index 首页模板

新增 templates/index.html:

1 {% extends 'base.html' %}
2 {% block title %}主页{% endblock %}
3 {% block content %}
4     {% if request.user.is_authenticated %}
5     <h1>你好,{{ request.user.username }}!欢迎回来!</h1>
6     {% endif %}
7 {% endblock %}

根据登陆状态的不一样,显示不一样的内容。

  

2. 项目

预期效果以下:

2.1 定义模型类

Django 的模型类提供以面向对象的方式管理数据库数据,即 ORM(关系对象映射)的思想。

1)编写模型类

打开应用目录下的 models.py,定义项目信息的模型类:

 1 from django.db import models
 2 
 3 
 4 class Project(models.Model):
 5     id = models.AutoField(primary_key=True)
 6     name = models.CharField('项目名称', max_length=50, unique=True, null=False)
 7     proj_owner = models.CharField('项目负责人', max_length=20, null=False)
 8     test_owner = models.CharField('测试负责人', max_length=20, null=False)
 9     dev_owner = models.CharField('开发负责人', max_length=20, null=False)
10     desc = models.CharField('项目描述', max_length=100, null=True)
11     create_time = models.DateTimeField('项目建立时间', auto_now_add=True)
12     update_time = models.DateTimeField('项目更新时间', auto_now=True, null=True)
13 
14     # 打印对象时返回项目名称
15     def __str__(self):
16         return self.name
17 
18     class Meta:
19         verbose_name = '项目信息表'
20         verbose_name_plural = '项目信息表'

字段说明:

  • name:项目名称,必填,最长不超过 128 个字符,而且惟一,也就是不能有相同项目名称。
  • proj_owner:项目负责人,必填,最长不超过 20 个字符(实际可能不须要这么长)。
  • test_owner:测试负责人。必填,最长不超过 20 个字符。
  • dev_owner:开发负责人,必填,最长不超过 20 个字符。
  • desc :项目描述,选填,最长不超过 100 个字符。
  • create_time:建立时间,必填,默认使用当前时间。
  • update_time:更新时间,选填,默认为当前时间。
  • 使用 __str__ 更友好地打印对象信息。
  • Meta 类属性 verbose_name 指定了后台 Admin 页面展现的表名称。

2)数据迁移

在项目目录下,执行如下两个命令进行数据迁移(将模型类转换成数据库表):

python manage.py makemigrations  # 生成迁移文件(模型类的信息)
python manage.py migrate  # 执行开始迁移(将模型类信息转换成数据库表)

从数据库迁移过程来看,迁移文件的内容是 Django 与 Mysql 之间的沟通语言,Django 经过 ORM(对象关系映射)功能,把模型类生成的迁移文件,映射到 Mysql 中,Mysql 经过迁移文件内容,建立对应的数据库表信息。

ORM 的好处就是,咱们不须要编写 SQL 语句,而是经过 Django 的模型类,就能够在 Mysql 中建立和操做想要的表对象及其数据。

除了模型类中定义的表,初次执行迁移命令也会生成 Django 内置的表,这些表是服务于 Django 内置的功能。

2)Admin 站点增长项目数据

在应用目录下的 admin.py 中,添加以下代码:

from django.contrib import admin
from .import models


class ProjectAdmin(admin.ModelAdmin):
    list_display = ("id", "name", "proj_owner", "test_owner", "dev_owner", "desc", "create_time", "update_time")

admin.site.register(models.Project, ProjectAdmin)

admin.site.register() 方法把模型类 Project 和 ProjectAdmin 进行绑定,并注册到 Admin 系统。经过 ProjectAdmin 类中的 list_display 字段,定义了后台 Admin 页面展现 Project 表的字段列表。 

进入 Admin 页面并添加数据:

能够看到,在应用名称下展现了“项目信息表,接着能够依次点击“项目信息表”—“ADD 项目信息表 +”—填写所需新增的项目信息—“SAVE”,完成项目信息的数据添加。

2.2 定义路由

添加项目管理页面的路由信息:

from django.urls import path
from . import views

urlpatterns = [
    path('',views.index),
    path('login/', views.login),
    path('logout/', views.logout),
    path('project/', views.project, name="project"),
]

2.3 定义视图函数

 1 from django.shortcuts import render, redirect
 2 from django.contrib import auth  # Django用户认证(Auth)组件通常用在用户的登陆注册上,用于判断当前的用户是否合法
 3 from django.contrib.auth.decorators import login_required
 4 from .form import UserForm
 5 import traceback
 6 from .models import Project
 7 
 8 
 9 # 项目管理页的视图函数
10 @login_required
11 def project(request):
12     print("request.user.is_authenticated: ", request.user.is_authenticated)  # 打印用户是否已登陆
13     projects = Project.objects.filter().order_by('-id')  # 使用负id是为了倒序取出项目数据
14     print("projects:", projects)  # 打印项目名称
15     return render(request, 'project.html', {'projects': projects})
16 
17 
18 # 默认页的视图函数
19 @login_required
20 def index(request):
21     return render(request, 'index.html')
22 
23 
24 # 登陆页的视图函数
25 def login(request):
26     print("request.session.items(): {}".format(request.session.items()))  # 打印session信息
27     if request.session.get('is_login', None):
28         return redirect('/')
29     # 若是是表单提交行为,则进行登陆校验
30     if request.method == "POST":
31         login_form = UserForm(request.POST)
32         message = "请检查填写的内容!"
33         if login_form.is_valid():
34             username = login_form.cleaned_data['username']
35             password = login_form.cleaned_data['password']
36             try:
37                 # 使用django提供的身份验证功能
38                 user = auth.authenticate(username=username, password=password)  # 从auth_user表中匹配信息,匹配到则返回用户对象
39                 if user is not None:
40                     print("用户【%s】登陆成功" % username)
41                     auth.login(request, user)
42                     request.session['is_login'] = True
43                     # 登陆成功,跳转主页
44                     return redirect('/')
45                 else:
46                     message = "用户名不存在或者密码不正确!"
47             except:
48                 traceback.print_exc()
49                 message = "登陆程序出现异常"
50         # 用户名或密码为空,返回登陆页和错误提示信息
51         else:
52             return render(request, 'login.html', locals())
53     # 不是表单提交,表明只是访问登陆页
54     else:
55         login_form = UserForm()
56         return render(request, 'login.html', locals())
57 
58 
59 # 注册页的视图函数
60 def register(request):
61     return render(request, 'register.html')
62 
63 
64 # 登出的视图函数:重定向至login视图函数
65 @login_required
66 def logout(request):
67     auth.logout(request)
68     request.session.flush()
69     return redirect("/login/")

2.4 定义模板文件

1)新增 templates/project.html 模板,用于展现项目信息:

 1 {% extends 'base.html' %}
 2 {% load static %}
 3 {% block title %}项目{% endblock %}
 4 
 5 {% block content %}
 6     <div class="table-responsive">
 7         <table class="table table-striped">
 8             <thead>
 9                 <tr>
10                     <th>id</th>
11                     <th>项目名称</th>
12                     <th>项目负责人</th>
13                     <th>测试负责人</th>
14                     <th>开发负责人</th>
15                     <th>简要描述</th>
16                     <th>建立时间</th>
17                     <th>更新时间</th>
18                     <th>测试结果统计</th>
19                 </tr>
20             </thead>
21             <tbody>
22 
23             {% for project in projects %}
24                 <tr>
25                     <td>{{ project.id }}</td>
26                     <td>{{ project.name }}</td>
27                     <td>{{ project.proj_owner }}</td>
28                     <td>{{ project.test_owner }}</td>
29                     <td>{{ project.dev_owner }}</td>
30                     <td>{{ project.desc }}</td>
31                     <td>{{ project.create_time|date:"Y-n-d H:i" }}</td>
32                     <td>{{ project.update_time|date:"Y-n-d H:i" }}</td>
33                     <td><a href=""> 查看</a></td>
34                 </tr>
35             {% endfor %}
36             </tbody>
37         </table>
38     </div>
39 {% endblock %}

2)调整 base.html 模板:导航栏增长项目菜单,把“主页”菜单改成“项目”,同时把“自动化测试平台”菜单链接调整为”/”,即首页页面。

 1 <!DOCTYPE html>
 2 <html lang="zh-CN">
 3 {% load static %}
 4 <head>
 5     <meta charset="utf-8">
 6     <meta http-equiv="X-UA-Compatible" content="IE=edge">
 7     <meta name="viewport" content="width=device-width, initial-scale=1">
 8     <!-- 上述3个meta标签*必须*放在最前面,任何其余内容都*必须*跟随其后! -->
 9     <title>{% block title %}base{% endblock %}</title>
10 
11     <!-- Bootstrap -->
12     <link href="{% static 'bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
13 
14 
15     <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
16     <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
17     <!--[if lt IE 9]>
18     <script src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
19     <script src="https://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
20     <![endif]-->
21     {% block css %}{% endblock %}
22 </head>
23 <body>
24 <nav class="navbar navbar-default">
25     <div class="container-fluid">
26         <!-- Brand and toggle get grouped for better mobile display -->
27         <div class="navbar-header">
28             <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#my-nav"
29                     aria-expanded="false">
30                 <span class="sr-only">切换导航条</span>
31                 <span class="icon-bar"></span>
32                 <span class="icon-bar"></span>
33                 <span class="icon-bar"></span>
34             </button>
35             <a class="navbar-brand" href="/">自动化测试平台</a>
36         </div>
37 
38         <div class="collapse navbar-collapse" id="my-nav">
39             <ul class="nav navbar-nav">
40                 <li class="active"><a href="/project/">项目</a></li>
41             </ul>
42             <ul class="nav navbar-nav navbar-right">
43                 {% if request.user.is_authenticated %}
44                 <li><a href="#">当前在线:{{ request.user.username }}</a></li>
45                 <li><a href="/logout">登出</a></li>
46                 {% else %}
47                 <li><a href="/login">登陆</a></li>
48 
49                 {% endif %}
50             </ul>
51         </div><!-- /.navbar-collapse -->
52     </div><!-- /.container-fluid -->
53 </nav>
54 
55 {% block content %}{% endblock %}
56 
57 
58 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
59 <script src="{% static 'js/jquery-3.4.1.js' %}"></script>
60 <!-- Include all compiled plugins (below), or include individual files as needed -->
61 <script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script>
62 </body>
63 </html>

2.5 处理分页 

1)新增封装分页处理对象的视图函数:

 1 from django.shortcuts import render, redirect, HttpResponse
 2 from django.contrib import auth  # Django用户认证(Auth)组件通常用在用户的登陆注册上,用于判断当前的用户是否合法
 3 from django.contrib.auth.decorators import login_required
 4 from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage
 5 from .form import UserForm
 6 import traceback
 7 from .models import Project
 8 
 9 
10 # 封装分页处理
11 def get_paginator(request, data):
12     paginator = Paginator(data, 10)  # 默认每页展现10条数据
13     # 获取 url 后面的 page 参数的值, 首页不显示 page 参数, 默认值是 1
14     page = request.GET.get('page')
15     try:
16         paginator_pages = paginator.page(page)
17     except PageNotAnInteger:
18         # 若是请求的页数不是整数, 返回第一页。
19         paginator_pages = paginator.page(1)
20     except InvalidPage:
21         # 若是请求的页数不存在, 重定向页面
22         return HttpResponse('找不到页面的内容')
23     return paginator_pages
24 
25 
26 @login_required
27 def project(request):
28     print("request.user.is_authenticated: ", request.user.is_authenticated)
29     projects = Project.objects.filter().order_by('-id')
30     print("projects:", projects)
31     return render(request, 'project.html', {'projects': get_paginator(request, projects)})
32 
33 
34 # 默认页的视图函数
35 @login_required
36 def index(request):
37     return render(request, 'index.html')
38 
39 
40 # 登陆页的视图函数
41 def login(request):
42     print("request.session.items(): {}".format(request.session.items()))  # 打印session信息
43     if request.session.get('is_login', None):
44         return redirect('/')
45     # 若是是表单提交行为,则进行登陆校验
46     if request.method == "POST":
47         login_form = UserForm(request.POST)
48         message = "请检查填写的内容!"
49         if login_form.is_valid():
50             username = login_form.cleaned_data['username']
51             password = login_form.cleaned_data['password']
52             try:
53                 # 使用django提供的身份验证功能
54                 user = auth.authenticate(username=username, password=password)  # 从auth_user表中匹配信息,匹配到则返回用户对象
55                 if user is not None:
56                     print("用户【%s】登陆成功" % username)
57                     auth.login(request, user)
58                     request.session['is_login'] = True
59                     # 登陆成功,跳转主页
60                     return redirect('/')
61                 else:
62                     message = "用户名不存在或者密码不正确!"
63             except:
64                 traceback.print_exc()
65                 message = "登陆程序出现异常"
66         # 用户名或密码为空,返回登陆页和错误提示信息
67         else:
68             return render(request, 'login.html', locals())
69     # 不是表单提交,表明只是访问登陆页
70     else:
71         login_form = UserForm()
72         return render(request, 'login.html', locals())
73 
74 
75 # 注册页的视图函数
76 def register(request):
77     return render(request, 'register.html')
78 
79 
80 # 登出的视图函数:重定向至login视图函数
81 @login_required
82 def logout(request):
83     auth.logout(request)
84     request.session.flush()
85     return redirect("/login/")

2)修改 project.html 模板,新增分页处理内容:

 1 {% extends 'base.html' %}
 2 {% load static %}
 3 {% block title %}主页{% endblock %}
 4 
 5 {% block content %}
 6 <div class="table-responsive">
 7     <table class="table table-striped">
 8         <thead>
 9         <tr>
10             <th>id</th>
11             <th>项目名称</th>
12             <th>项目负责人</th>
13             <th>测试负责人</th>
14             <th>开发负责人</th>
15             <th>简要描述</th>
16             <th>建立时间</th>
17             <th>更新时间</th>
18             <th>测试结果统计</th>
19         </tr>
20         </thead>
21         <tbody>
22 
23         {% for project in projects %}
24         <tr>
25             <td>{{ project.id }}</td>
26             <td>{{ project.name }}</td>
27             <td>{{ project.proj_owner }}</td>
28             <td>{{ project.test_owner }}</td>
29             <td>{{ project.dev_owner }}</td>
30             <td>{{ project.desc }}</td>
31             <td>{{ project.create_time|date:"Y-n-d H:i" }}</td>
32             <td>{{ project.update_time|date:"Y-n-d H:i" }}</td>
33             <td><a href=""> 查看</a></td>
34         </tr>
35         {% endfor %}
36         </tbody>
37     </table>
38 </div>
39 
40 {# 实现分页标签的代码 #}
41 {# 这里使用 bootstrap 渲染页面 #}
42 <div id="pages" class="text-center">
43     <nav>
44         <ul class="pagination">
45             <li class="step-links">
46                 {% if projects.has_previous %}
47                 <a class='active' href="?page={{ projects.previous_page_number }}">上一页</a>
48                 {% endif %}
49 
50                 <span class="current">
51                         第 {{ projects.number }} 页 / 共 {{ projects.paginator.num_pages }} 页</span>
52 
53                 {% if projects.has_next %}
54                 <a class='active' href="?page={{ projects.next_page_number }}">下一页</a>
55                 {% endif %}
56             </li>
57         </ul>
58     </nav>
59 </div>
60 {% endblock %}

 

3. 模块

预期效果以下:

模块管理的实现思路与项目管理类似,咱们依然遵循模型类 —> admin 增长数据 —> 路由 —> 视图函数 —> 模板文件的步骤来实现。

3.1 定义模型类 

1)应用的 models.py 中增长 Module 模型类:

 1 from django.db import models
 2 
 3 
 4 class Project(models.Model):
 5     id = models.AutoField(primary_key=True)
 6     name = models.CharField('项目名称', max_length=50, unique=True, null=False)
 7     proj_owner = models.CharField('项目负责人', max_length=20, null=False)
 8     test_owner = models.CharField('测试负责人', max_length=20, null=False)
 9     dev_owner = models.CharField('开发负责人', max_length=20, null=False)
10     desc = models.CharField('项目描述', max_length=100, null=True)
11     create_time = models.DateTimeField('项目建立时间', auto_now_add=True)
12     update_time = models.DateTimeField('项目更新时间', auto_now=True, null=True)
13 
14     # 打印时返回项目名称
15     def __str__(self):
16         return self.name
17 
18     class Meta:
19         verbose_name = '项目信息表'
20         verbose_name_plural = '项目信息表'
21 
22 
23 class Module(models.Model):
24     id = models.AutoField(primary_key=True)
25     name = models.CharField('模块名称', max_length=50, null=False)
26     belong_project = models.ForeignKey(Project, on_delete=models.CASCADE)
27     test_owner = models.CharField('测试负责人', max_length=50, null=False)
28     desc = models.CharField('简要描述', max_length=100, null=True)
29     create_time = models.DateTimeField('建立时间', auto_now_add=True)
30     update_time = models.DateTimeField('更新时间', auto_now=True, null=True)
31 
32     def __str__(self):
33         return self.name
34 
35     class Meta:
36         verbose_name = '模块信息表'
37         verbose_name_plural = '模块信息表'

2)数据迁移

在项目目录下,执行如下两个命令进行数据迁移(将模型类转换成数据库表):

python manage.py makemigrations  # 生成迁移文件(模型类的信息)
python manage.py migrate  # 执行开始迁移(将模型类信息转换成数据库表)

3.2 后台 admin 添加数据

1)注册模型类到 admin:

 1 from django.contrib import admin
 2 from . import models
 3 
 4 
 5 class ProjectAdmin(admin.ModelAdmin):
 6     list_display = ("id", "name", "proj_owner", "test_owner", "dev_owner", "desc", "create_time", "update_time")
 7 
 8 admin.site.register(models.Project, ProjectAdmin)
 9 
10 
11 class ModuleAdmin(admin.ModelAdmin):
12     list_display = ("id", "name", "belong_project", "test_owner", "desc", "create_time", "update_time")
13 
14 admin.site.register(models.Module, ModuleAdmin)

2)登陆 admin 系统,添加模块数据

访问:http://127.0.0.1:8000/admin/,进入模块信息表,添加数据。

 

3.3 定义路由 

新增应用 urls.py 的路由配置:

from django.urls import path
from . import views

urlpatterns = [
    path('',views.index),
    path('login/', views.login),
    path('logout/', views.logout),
    path('project/', views.project, name="project"),
    path('module/', views.module, name="module"),
]

路由地址“module”对应的视图函数,指向 views.py 中的 module 方法,下一步咱们添加下该方法的处理。

3.4 定义视图函数

 1 from django.shortcuts import render, redirect, HttpResponse
 2 from django.contrib import auth  # Django用户认证(Auth)组件通常用在用户的登陆注册上,用于判断当前的用户是否合法
 3 from django.contrib.auth.decorators import login_required
 4 from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage
 5 from .form import UserForm
 6 import traceback
 7 from .models import Project, Module
 8 
 9 
10 # 封装分页处理
11 def get_paginator(request, data):
12     paginator = Paginator(data, 10)  # 默认每页展现10条数据
13     # 获取 url 后面的 page 参数的值, 首页不显示 page 参数, 默认值是 1
14     page = request.GET.get('page')
15     try:
16         paginator_pages = paginator.page(page)
17     except PageNotAnInteger:
18         # 若是请求的页数不是整数, 返回第一页。
19         paginator_pages = paginator.page(1)
20     except InvalidPage:
21         # 若是请求的页数不存在, 重定向页面
22         return HttpResponse('找不到页面的内容')
23     return paginator_pages
24 
25 
26 @login_required
27 def project(request):
28     print("request.user.is_authenticated: ", request.user.is_authenticated)
29     projects = Project.objects.filter().order_by('-id')
30     print("projects:", projects)
31     return render(request, 'project.html', {'projects': get_paginator(request, projects)})
32 
33 
34 @login_required
35 def module(request):
36     if request.method == "GET":  # 请求get时候,id倒序查询全部的模块数据
37         modules = Module.objects.filter().order_by('-id')
38         return render(request, 'module.html', {'modules': get_paginator(request, modules)})
39     else:  # 不然就是Post请求,会根据输入内容,使用模糊的方式查找全部的项目
40         proj_name = request.POST['proj_name']
41         projects = Project.objects.filter(name__contains=proj_name.strip())
42         projs = [proj.id for proj in projects]
43         modules = Module.objects.filter(belong_project__in=projs)  # 把项目中全部的模块都找出来
44         return render(request, 'module.html', {'modules': get_paginator(request, modules), 'proj_name': proj_name})
45 
46 
47 # 默认页的视图函数
48 @login_required
49 def index(request):
50     return render(request, 'index.html')
51 
52 
53 # 登陆页的视图函数
54 def login(request):
55     print("request.session.items(): {}".format(request.session.items()))  # 打印session信息
56     if request.session.get('is_login', None):
57         return redirect('/')
58     # 若是是表单提交行为,则进行登陆校验
59     if request.method == "POST":
60         login_form = UserForm(request.POST)
61         message = "请检查填写的内容!"
62         if login_form.is_valid():
63             username = login_form.cleaned_data['username']
64             password = login_form.cleaned_data['password']
65             try:
66                 # 使用django提供的身份验证功能
67                 user = auth.authenticate(username=username, password=password)  # 从auth_user表中匹配信息,匹配到则返回用户对象
68                 if user is not None:
69                     print("用户【%s】登陆成功" % username)
70                     auth.login(request, user)
71                     request.session['is_login'] = True
72                     # 登陆成功,跳转主页
73                     return redirect('/')
74                 else:
75                     message = "用户名不存在或者密码不正确!"
76             except:
77                 traceback.print_exc()
78                 message = "登陆程序出现异常"
79         # 用户名或密码为空,返回登陆页和错误提示信息
80         else:
81             return render(request, 'login.html', locals())
82     # 不是表单提交,表明只是访问登陆页
83     else:
84         login_form = UserForm()
85         return render(request, 'login.html', locals())
86 
87 
88 # 注册页的视图函数
89 def register(request):
90     return render(request, 'register.html')
91 
92 
93 # 登出的视图函数:重定向至login视图函数
94 @login_required
95 def logout(request):
96     auth.logout(request)
97     request.session.flush()
98     return redirect("/login/")

3.5 定义模板

1)新增 templates/module.html 模板

 1 {% extends 'base.html' %}
 2 {% load static %}
 3 {% block title %}模块{% endblock %}
 4 
 5 {% block content %}
 6 <form action="{% url 'module'%}" method="POST">
 7     {% csrf_token %}
 8     <input style="margin-left: 5px;" type="text" name="proj_name" value="{{ proj_name }}" placeholder="输入项目名称搜索模块">
 9     <input type="submit" value="搜索">
10 </form>
11 
12 <div class="table-responsive">
13 
14     <table class="table table-striped">
15         <thead>
16         <tr>
17             <th>id</th>
18             <th>模块名称</th>
19             <th>所属项目</th>
20             <th>测试负责人</th>
21             <th>模块描述</th>
22             <th>建立时间</th>
23             <th>更新时间</th>
24             <th>测试结果统计</th>
25         </tr>
26         </thead>
27         <tbody>
28 
29         {% for module in modules %}
30         <tr>
31             <td>{{ module.id }}</td>
32             <td><a href="">{{ module.name }}</a></td>
33             <td>{{ module.belong_project.name }}</td>
34             <td>{{ module.test_owner }}</td>
35             <td>{{ module.desc }}</td>
36             <td>{{ module.create_time|date:"Y-n-d H:i" }}</td>
37             <td>{{ module.update_time|date:"Y-n-d H:i" }}</td>
38             <td><a href="">查看</a></td>
39         </tr>
40         {% endfor %}
41         
42         </tbody>
43     </table>
44 </div>
45 
46 {# 实现分页标签的代码 #}
47 {# 这里使用 bootstrap 渲染页面 #}
48 <div id="pages" class="text-center">
49     <nav>
50         <ul class="pagination">
51             <li class="step-links">
52                 {% if modules.has_previous %}
53                 <a class='active' href="?page={{ modules.previous_page_number }}">上一页</a>
54                 {% endif %}
55 
56                 <span class="current">
57                     第 {{ modules.number }} 页 / 共 {{ modules.paginator.num_pages }} 页</span>
58 
59                 {% if modules.has_next %}
60                 <a class='active' href="?page={{ modules.next_page_number }}">下一页</a>
61                 {% endif %}
62             </li>
63         </ul>
64     </nav>
65 </div>
66 {% endblock %}

2)修改 base.html 模板,新增模块菜单栏:

 1 <!DOCTYPE html>
 2 <html lang="zh-CN">
 3 {% load static %}
 4 <head>
 5     <meta charset="utf-8">
 6     <meta http-equiv="X-UA-Compatible" content="IE=edge">
 7     <meta name="viewport" content="width=device-width, initial-scale=1">
 8     <!-- 上述3个meta标签*必须*放在最前面,任何其余内容都*必须*跟随其后! -->
 9     <title>{% block title %}base{% endblock %}</title>
10 
11     <!-- Bootstrap -->
12     <link href="{% static 'bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
13 
14 
15     <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
16     <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
17     <!--[if lt IE 9]>
18     <script src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
19     <script src="https://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
20     <![endif]-->
21     {% block css %}{% endblock %}
22 </head>
23 <body>
24 <nav class="navbar navbar-default">
25     <div class="container-fluid">
26         <!-- Brand and toggle get grouped for better mobile display -->
27         <div class="navbar-header">
28             <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#my-nav"
29                     aria-expanded="false">
30                 <span class="sr-only">切换导航条</span>
31                 <span class="icon-bar"></span>
32                 <span class="icon-bar"></span>
33                 <span class="icon-bar"></span>
34             </button>
35             <a class="navbar-brand" href="/">自动化测试平台</a>
36         </div>
37 
38         <div class="collapse navbar-collapse" id="my-nav">
39             <ul class="nav navbar-nav">
40                 <li class="active"><a href="/project/">项目</a></li>
41                 <li class="active"><a href="/module/">模块</a></li>
42             </ul>
43             <ul class="nav navbar-nav navbar-right">
44                 {% if request.user.is_authenticated %}
45                 <li><a href="#">当前在线:{{ request.user.username }}</a></li>
46                 <li><a href="/logout">登出</a></li>
47                 {% else %}
48                 <li><a href="/login">登陆</a></li>
49 
50                 {% endif %}
51             </ul>
52         </div><!-- /.navbar-collapse -->
53     </div><!-- /.container-fluid -->
54 </nav>
55 
56 {% block content %}{% endblock %}
57 
58 
59 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
60 <script src="{% static 'js/jquery-3.4.1.js' %}"></script>
61 <!-- Include all compiled plugins (below), or include individual files as needed -->
62 <script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script>
63 </body>
64 </html>

 

4. 测试用例

4.1 定义模型类

1)在应用 models.py 中增长 TestCase 模型类:

 1 from django.db import models
 2 from smart_selects.db_fields import GroupedForeignKey  # 后台级联选择
 3 from django.contrib.auth.models import User
 4 
 5 
 6 class Project(models.Model):
 7     id = models.AutoField(primary_key=True)
 8     name = models.CharField('项目名称', max_length=50, unique=True, null=False)
 9     proj_owner = models.CharField('项目负责人', max_length=20, null=False)
10     test_owner = models.CharField('测试负责人', max_length=20, null=False)
11     dev_owner = models.CharField('开发负责人', max_length=20, null=False)
12     desc = models.CharField('项目描述', max_length=100, null=True)
13     create_time = models.DateTimeField('项目建立时间', auto_now_add=True)
14     update_time = models.DateTimeField('项目更新时间', auto_now=True, null=True)
15 
16     def __str__(self):
17         return self.name
18 
19     class Meta:
20         verbose_name = '项目信息表'
21         verbose_name_plural = '项目信息表'
22 
23 
24 class Module(models.Model):
25     id = models.AutoField(primary_key=True)
26     name = models.CharField('模块名称', max_length=50, null=False)
27     belong_project = models.ForeignKey(Project, on_delete=models.CASCADE)
28     test_owner = models.CharField('测试负责人', max_length=50, null=False)
29     desc = models.CharField('简要描述', max_length=100, null=True)
30     create_time = models.DateTimeField('建立时间', auto_now_add=True)
31     update_time = models.DateTimeField('更新时间', auto_now=True, null=True)
32 
33     def __str__(self):
34         return self.name
35 
36     class Meta:
37         verbose_name = '模块信息表'
38         verbose_name_plural = '模块信息表'
39 
40 
41 class TestCase(models.Model):
42     id = models.AutoField(primary_key=True)
43     case_name = models.CharField('用例名称', max_length=50, null=False)  # 如 register
44     belong_project = models.ForeignKey(Project, on_delete=models.CASCADE, verbose_name='所属项目')
45     belong_module = GroupedForeignKey(Module, "belong_project", on_delete=models.CASCADE, verbose_name='所属模块')
46     request_data = models.CharField('请求数据', max_length=1024, null=False, default='')
47     uri = models.CharField('接口地址', max_length=1024, null=False, default='')
48     assert_key = models.CharField('断言内容', max_length=1024, null=True)
49     maintainer = models.CharField('编写人员', max_length=1024, null=False, default='')
50     extract_var = models.CharField('提取变量表达式', max_length=1024, null=True)  # 示例:userid||userid": (\d+)
51     request_method = models.CharField('请求方式', max_length=1024, null=True)
52     status = models.IntegerField(null=True, help_text="0:表示有效,1:表示无效,用于软删除")
53     created_time = models.DateTimeField('建立时间', auto_now_add=True)
54     updated_time = models.DateTimeField('更新时间', auto_now=True, null=True)
55     user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='责任人', null=True)
56 
57     def __str__(self):
58         return self.case_name
59 
60     class Meta:
61         verbose_name = '测试用例表'
62         verbose_name_plural = '测试用例表'

GroupedForeignKey 能够支持在 admin 新增数据时,展现该模型类的关联表数据。(需提早安装:pip install django-smart-selects)

2)数据迁移

在项目目录下,执行如下两个命令进行数据迁移(将模型类转换成数据库表):

python manage.py makemigrations  # 生成迁移文件(模型类的信息)
python manage.py migrate  # 执行开始迁移(将模型类信息转换成数据库表)

4.2 后台 admin 添加数据

1)注册模型类到 admin

应用 admin.py 文件中增长以下代码:注册 TestCase 模型类到 admin 后台系统。

 1 from django.contrib import admin
 2 from . import models
 3 
 4 
 5 class ProjectAdmin(admin.ModelAdmin):
 6     list_display = ("id", "name", "proj_owner", "test_owner", "dev_owner", "desc", "create_time", "update_time")
 7 
 8 admin.site.register(models.Project, ProjectAdmin)
 9 
10 
11 class ModuleAdmin(admin.ModelAdmin):
12     list_display = ("id", "name", "belong_project", "test_owner", "desc", "create_time", "update_time")
13 
14 admin.site.register(models.Module, ModuleAdmin)
15 
16 
17 class TestCaseAdmin(admin.ModelAdmin):
18     list_display = (
19         "id", "case_name", "belong_project", "belong_module", "request_data", "uri", "assert_key", "maintainer",
20         "extract_var", "request_method", "status", "created_time", "updated_time", "user")
21 
22 admin.site.register(models.TestCase, TestCaseAdmin)

2)登陆 admin 系统,添加用例数据

访问 http://127.0.0.1:8000/admin/,进入测试用例表,添加数据:

添加用例数据时,页面以下:

  • 所属项目和所属模块下拉选项是根据模型类中的 GroupedForeignKey 属性生成的,方便咱们正确的关联数据。 
  • 请求数据、断言内容、提取变量表达式等字段的定义,须要根据接口业务逻辑,以及后续运行逻辑的设计来输入。 

 

添加数据后以下所示: 

4.3 定义路由 

应用 urls.py:

from django.urls import path
from . import views


urlpatterns = [
    path('', views.index),
    path('login/', views.login),
    path('logout/', views.logout),
    path('project/', views.project, name='project'),
    path('module/', views.module, name='module'),
    path('test_case/', views.test_case, name="test_case"),
]

4.4 定义视图函数

  1 from django.shortcuts import render, redirect, HttpResponse
  2 from django.contrib import auth  # Django用户认证(Auth)组件通常用在用户的登陆注册上,用于判断当前的用户是否合法
  3 from django.contrib.auth.decorators import login_required
  4 from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage
  5 from .form import UserForm
  6 import traceback
  7 from .models import Project, Module, TestCase
  8 
  9 
 10 # 封装分页处理
 11 def get_paginator(request, data):
 12     paginator = Paginator(data, 10)  # 默认每页展现10条数据
 13     # 获取 url 后面的 page 参数的值, 首页不显示 page 参数, 默认值是 1
 14     page = request.GET.get('page')
 15     try:
 16         paginator_pages = paginator.page(page)
 17     except PageNotAnInteger:
 18         # 若是请求的页数不是整数, 返回第一页。
 19         paginator_pages = paginator.page(1)
 20     except InvalidPage:
 21         # 若是请求的页数不存在, 重定向页面
 22         return HttpResponse('找不到页面的内容')
 23     return paginator_pages
 24 
 25 
 26 # 项目菜单
 27 @login_required
 28 def project(request):
 29     print("request.user.is_authenticated: ", request.user.is_authenticated)
 30     projects = Project.objects.filter().order_by('-id')
 31     print("projects:", projects)
 32     return render(request, 'project.html', {'projects': get_paginator(request, projects)})
 33 
 34 
 35 # 模块菜单
 36 @login_required
 37 def module(request):
 38     if request.method == "GET":  # 请求get时候,id倒序查询全部的模块数据
 39         modules = Module.objects.filter().order_by('-id')
 40         return render(request, 'module.html', {'modules': get_paginator(request, modules)})
 41     else:  # 不然就是Post请求,会根据输入内容,使用模糊的方式查找全部的项目
 42         proj_name = request.POST['proj_name']
 43         projects = Project.objects.filter(name__contains=proj_name.strip())
 44         projs = [proj.id for proj in projects]
 45         modules = Module.objects.filter(belong_project__in=projs)  # 把项目中全部的模块都找出来
 46         return render(request, 'module.html', {'modules': get_paginator(request, modules), 'proj_name': proj_name})
 47 
 48 
 49 # 测试用例菜单
 50 @login_required
 51 def test_case(request):
 52     print("request.session['is_login']: {}".format(request.session['is_login']))
 53     test_cases = ""
 54     if request.method == "GET":
 55         test_cases = TestCase.objects.filter().order_by('id')
 56         print("testcases in testcase: {}".format(test_cases))
 57     elif request.method == "POST":
 58         print("request.POST: {}".format(request.POST))
 59         test_case_id_list = request.POST.getlist('testcases_list')
 60         if test_case_id_list:
 61             test_case_id_list.sort()
 62             print("test_case_id_list: {}".format(test_case_id_list))
 63         test_cases = TestCase.objects.filter().order_by('id')
 64     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
 65 
 66 
 67 # 默认页的视图函数
 68 @login_required
 69 def index(request):
 70     return render(request, 'index.html')
 71 
 72 
 73 # 登陆页的视图函数
 74 def login(request):
 75     print("request.session.items(): {}".format(request.session.items()))  # 打印session信息
 76     if request.session.get('is_login', None):
 77         return redirect('/')
 78     # 若是是表单提交行为,则进行登陆校验
 79     if request.method == "POST":
 80         login_form = UserForm(request.POST)
 81         message = "请检查填写的内容!"
 82         if login_form.is_valid():
 83             username = login_form.cleaned_data['username']
 84             password = login_form.cleaned_data['password']
 85             try:
 86                 # 使用django提供的身份验证功能
 87                 user = auth.authenticate(username=username, password=password)  # 从auth_user表中匹配信息,匹配到则返回用户对象
 88                 if user is not None:
 89                     print("用户【%s】登陆成功" % username)
 90                     auth.login(request, user)
 91                     request.session['is_login'] = True
 92                     # 登陆成功,跳转主页
 93                     return redirect('/')
 94                 else:
 95                     message = "用户名不存在或者密码不正确!"
 96             except:
 97                 traceback.print_exc()
 98                 message = "登陆程序出现异常"
 99         # 用户名或密码为空,返回登陆页和错误提示信息
100         else:
101             return render(request, 'login.html', locals())
102     # 不是表单提交,表明只是访问登陆页
103     else:
104         login_form = UserForm()
105         return render(request, 'login.html', locals())
106 
107 
108 # 注册页的视图函数
109 def register(request):
110     return render(request, 'register.html')
111 
112 
113 # 登出的视图函数:重定向至login视图函数
114 @login_required
115 def logout(request):
116     auth.logout(request)
117     request.session.flush()
118     return redirect("/login/")

4.5 定义模板文件

1)新增 templates/test_case.html 模板:

 1 {% extends 'base.html' %}
 2 {% load static %}
 3 {% block title %}测试用例{% endblock %}
 4 
 5 {% block content %}
 6 <form action="" method="POST">
 7     {% csrf_token %}
 8     <div class="table-responsive">
 9         <table class="table table-striped">
10             <thead>
11             <tr>
12                 <th>用例名称</th>
13                 <th>所属项目</th>
14                 <th>所属模块</th>
15                 <th>接口地址</th>
16                 <th>请求方式</th>
17                 <th>请求数据</th>
18                 <th>断言key</th>
19                 <th>提取变量表达式</th>
20             </tr>
21             </thead>
22             <tbody>
23 
24             {% for test_case in test_cases %}
25             <tr>
26                 <td><a href="{% url 'test_case_detail' test_case.id%}">{{ test_case.case_name }}</a></td>
27                 <td>{{ test_case.belong_project.name }}</td>
28                 <td>{{ test_case.belong_module.name }}</td>
29                 <td>{{ test_case.uri }}</td>
30                 <td>{{ test_case.request_method }}</td>
31                 <td>{{ test_case.request_data }}</td>
32                 <td>{{ test_case.assert_key }}</td>
33                 <td>{{ test_case.extract_var }}</td>
34             </tr>
35             {% endfor %}
36             </tbody>
37         </table>
38 
39     </div>
40 </form>
41 {# 实现分页标签的代码 #}
42 {# 这里使用 bootstrap 渲染页面 #}
43 <div id="pages" class="text-center">
44     <nav>
45         <ul class="pagination">
46             <li class="step-links">
47                 {% if test_cases.has_previous %}
48                 <a class='active' href="?page={{ test_cases.previous_page_number }}">上一页</a>
49                 {% endif %}
50 
51                 <span class="current">
52                     第 {{ test_cases.number }} 页 / 共 {{ test_cases.paginator.num_pages }} 页</span>
53 
54                 {% if test_cases.has_next %}
55                 <a class='active' href="?page={{ test_cases.next_page_number }}">下一页</a>
56                 {% endif %}
57             </li>
58         </ul>
59     </nav>
60 </div>
61 {% endblock %}

2)修改 base.html 模板:增长测试用例菜单。

 1 <!DOCTYPE html>
 2 <html lang="zh-CN">
 3 {% load static %}
 4 <head>
 5     <meta charset="utf-8">
 6     <meta http-equiv="X-UA-Compatible" content="IE=edge">
 7     <meta name="viewport" content="width=device-width, initial-scale=1">
 8     <!-- 上述3个meta标签*必须*放在最前面,任何其余内容都*必须*跟随其后! -->
 9     <title>{% block title %}base{% endblock %}</title>
10 
11     <!-- Bootstrap -->
12     <link href="{% static 'bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
13 
14 
15     <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
16     <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
17     <!--[if lt IE 9]>
18     <script src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
19     <script src="https://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
20     <![endif]-->
21     {% block css %}{% endblock %}
22 </head>
23 <body>
24 <nav class="navbar navbar-default">
25     <div class="container-fluid">
26         <!-- Brand and toggle get grouped for better mobile display -->
27         <div class="navbar-header">
28             <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#my-nav"
29                     aria-expanded="false">
30                 <span class="sr-only">切换导航条</span>
31                 <span class="icon-bar"></span>
32                 <span class="icon-bar"></span>
33                 <span class="icon-bar"></span>
34             </button>
35             <a class="navbar-brand" href="/">自动化测试平台</a>
36         </div>
37 
38         <div class="collapse navbar-collapse" id="my-nav">
39             <ul class="nav navbar-nav">
40                 <li class="active"><a href="/project/">项目</a></li>
41                 <li class="active"><a href="/module/">模块</a></li>
42                 <li class="active"><a href="/test_case/">测试用例</a></li>
43             </ul>
44             <ul class="nav navbar-nav navbar-right">
45                 {% if request.user.is_authenticated %}
46                 <li><a href="#">当前在线:{{ request.user.username }}</a></li>
47                 <li><a href="/logout">登出</a></li>
48                 {% else %}
49                 <li><a href="/login">登陆</a></li>
50 
51                 {% endif %}
52             </ul>
53         </div><!-- /.navbar-collapse -->
54     </div><!-- /.container-fluid -->
55 </nav>
56 
57 {% block content %}{% endblock %}
58 
59 
60 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
61 <script src="{% static 'js/jquery-3.4.1.js' %}"></script>
62 <!-- Include all compiled plugins (below), or include individual files as needed -->
63 <script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script>
64 </body>
65 </html>

4.6 用例详情

目前用例列表中的字段仅包含用例的基本信息,下面继续加一下(点击用例名称跳转)用例详情页面,用于展现用例的所有字段信息,如建立时间、更新时间、维护人、建立人。

1)新增用例详情的路由配置

from django.urls import path, re_path
from . import views


urlpatterns = [
    path('', views.index),
    path('login/', views.login),
    path('logout/', views.logout),
    path('project/', views.project, name='project'),
    path('module/', views.module, name='module'),
    path('test_case/', views.test_case, name="test_case"),
    re_path('test_case_detail/(?P<test_case_id>[0-9]+)', views.test_case_detail, name="test_case_detail"),
]

因为用例详情路由地址须要传路由变量“test_case_id”,该变量须要经过正则表达式进行匹配,在 Django2.0 后,路由地址用到正则时须要用到 re_path 来解析,便于 Django 正确的匹配视图函数,以及在浏览器地址栏正确的展现 url 地址。

2)增长视图函数

  1 from django.shortcuts import render, redirect, HttpResponse
  2 from django.contrib import auth  # Django用户认证(Auth)组件通常用在用户的登陆注册上,用于判断当前的用户是否合法
  3 from django.contrib.auth.decorators import login_required
  4 from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage
  5 from .form import UserForm
  6 import traceback
  7 from .models import Project, Module, TestCase
  8 
  9 
 10 # 封装分页处理
 11 def get_paginator(request, data):
 12     paginator = Paginator(data, 10)  # 默认每页展现10条数据
 13     # 获取 url 后面的 page 参数的值, 首页不显示 page 参数, 默认值是 1
 14     page = request.GET.get('page')
 15     try:
 16         paginator_pages = paginator.page(page)
 17     except PageNotAnInteger:
 18         # 若是请求的页数不是整数, 返回第一页。
 19         paginator_pages = paginator.page(1)
 20     except InvalidPage:
 21         # 若是请求的页数不存在, 重定向页面
 22         return HttpResponse('找不到页面的内容')
 23     return paginator_pages
 24 
 25 
 26 # 项目菜单
 27 @login_required
 28 def project(request):
 29     print("request.user.is_authenticated: ", request.user.is_authenticated)
 30     projects = Project.objects.filter().order_by('-id')
 31     print("projects:", projects)
 32     return render(request, 'project.html', {'projects': get_paginator(request, projects)})
 33 
 34 
 35 # 模块菜单
 36 @login_required
 37 def module(request):
 38     if request.method == "GET":  # 请求get时候,id倒序查询全部的模块数据
 39         modules = Module.objects.filter().order_by('-id')
 40         return render(request, 'module.html', {'modules': get_paginator(request, modules)})
 41     else:  # 不然就是Post请求,会根据输入内容,使用模糊的方式查找全部的项目
 42         proj_name = request.POST['proj_name']
 43         projects = Project.objects.filter(name__contains=proj_name.strip())
 44         projs = [proj.id for proj in projects]
 45         modules = Module.objects.filter(belong_project__in=projs)  # 把项目中全部的模块都找出来
 46         return render(request, 'module.html', {'modules': get_paginator(request, modules), 'proj_name': proj_name})
 47 
 48 
 49 # 测试用例菜单
 50 @login_required
 51 def test_case(request):
 52     print("request.session['is_login']: {}".format(request.session['is_login']))
 53     test_cases = ""
 54     if request.method == "GET":
 55         test_cases = TestCase.objects.filter().order_by('id')
 56         print("testcases in testcase: {}".format(test_cases))
 57     elif request.method == "POST":
 58         print("request.POST: {}".format(request.POST))
 59         test_case_id_list = request.POST.getlist('testcases_list')
 60         if test_case_id_list:
 61             test_case_id_list.sort()
 62             print("test_case_id_list: {}".format(test_case_id_list))
 63         test_cases = TestCase.objects.filter().order_by('id')
 64     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
 65 
 66 
 67 # 用例详情
 68 @login_required
 69 def test_case_detail(request, test_case_id):
 70     test_case_id = int(test_case_id)
 71     test_case = TestCase.objects.get(id=test_case_id)
 72     print("test_case: {}".format(test_case))
 73     print("test_case.id: {}".format(test_case.id))
 74     print("test_case.belong_project: {}".format(test_case.belong_project))
 75 
 76     return render(request, 'test_case_detail.html', {'test_case': test_case})
 77 
 78 
 79 # 默认页的视图函数
 80 @login_required
 81 def index(request):
 82     return render(request, 'index.html')
 83 
 84 
 85 # 登陆页的视图函数
 86 def login(request):
 87     print("request.session.items(): {}".format(request.session.items()))  # 打印session信息
 88     if request.session.get('is_login', None):
 89         return redirect('/')
 90     # 若是是表单提交行为,则进行登陆校验
 91     if request.method == "POST":
 92         login_form = UserForm(request.POST)
 93         message = "请检查填写的内容!"
 94         if login_form.is_valid():
 95             username = login_form.cleaned_data['username']
 96             password = login_form.cleaned_data['password']
 97             try:
 98                 # 使用django提供的身份验证功能
 99                 user = auth.authenticate(username=username, password=password)  # 从auth_user表中匹配信息,匹配到则返回用户对象
100                 if user is not None:
101                     print("用户【%s】登陆成功" % username)
102                     auth.login(request, user)
103                     request.session['is_login'] = True
104                     # 登陆成功,跳转主页
105                     return redirect('/')
106                 else:
107                     message = "用户名不存在或者密码不正确!"
108             except:
109                 traceback.print_exc()
110                 message = "登陆程序出现异常"
111         # 用户名或密码为空,返回登陆页和错误提示信息
112         else:
113             return render(request, 'login.html', locals())
114     # 不是表单提交,表明只是访问登陆页
115     else:
116         login_form = UserForm()
117         return render(request, 'login.html', locals())
118 
119 
120 # 注册页的视图函数
121 def register(request):
122     return render(request, 'register.html')
123 
124 
125 # 登出的视图函数:重定向至login视图函数
126 @login_required
127 def logout(request):
128     auth.logout(request)
129     request.session.flush()
130     return redirect("/login/")

3)新增 templates/test_case_detail.html 模板

 1 {% extends 'base.html' %}
 2 {% load static %}
 3 {% block title %}用例详情{% endblock %}
 4 
 5 {% block content %}
 6 <div class="table-responsive">
 7     <table class="table table-striped">
 8         <thead>
 9         <tr>
10             <th width="3%">id</th>
11             <th width="4%">接口名称</th>
12             <th width="6%">所属项目</th>
13             <th width="6%">所属模块</th>
14             <th width="6%">接口地址</th>
15             <th width="10%">请求数据</th>
16             <th width="8%">断言内容</th>
17             <th width="4%">编写人员</th>
18             <th width="8%">提取变量表达式</th>
19             <th width="4%">维护人</th>
20             <th width="4%">建立人</th>
21             <th width="6%">建立时间</th>
22             <th width="6%">更新时间</th>
23         </tr>
24         </thead>
25         <tbody>
26         <tr>
27             <td>{{ test_case.id }}</td>
28             <td>{{ test_case.case_name }}</td>
29             <td>{{ test_case.belong_project }}</td>
30             <td>{{ test_case.belong_module }}</td>
31             <td>{{ test_case.uri }}</td>
32             <td>{{ test_case.request_data }}</td>
33             <td>{{ test_case.assert_key }}</td>
34             <td>{{ test_case.maintainer }}</td>
35             <td>{{ test_case.extract_var }}</td>
36             <td>{{ test_case.maintainer }}</td>
37             <td>{{ test_case.user.username }}</td>
38             <td>{{ test_case.created_time|date:"Y-n-d H:i" }}</td>
39             <td>{{ test_case.updated_time|date:"Y-n-d H:i" }}</td>
40         </tr>
41         </tbody>
42     </table>
43 </div>
44 {% endblock %}

4)修改 test_case.html 模板

添加用例名称的连接为用例详情路由地址:

 1 {% extends 'base.html' %}
 2 {% load static %}
 3 {% block title %}测试用例{% endblock %}
 4 
 5 {% block content %}
 6 <form action="" method="POST">
 7     {% csrf_token %}
 8     <div class="table-responsive">
 9         <table class="table table-striped">
10             <thead>
11             <tr>
12                 <th>用例名称</th>
13                 <th>所属项目</th>
14                 <th>所属模块</th>
15                 <th>接口地址</th>
16                 <th>请求方式</th>
17                 <th>请求数据</th>
18                 <th>断言key</th>
19                 <th>提取变量表达式</th>
20             </tr>
21             </thead>
22             <tbody>
23 
24             {% for test_case in test_cases %}
25             <tr>
26                 <td><a href="{% url 'test_case_detail' test_case.id%}">{{ test_case.case_name }}</a></td>
27                 <td>{{ test_case.belong_project.name }}</td>
28                 <td>{{ test_case.belong_module.name }}</td>
29                 <td>{{ test_case.uri }}</td>
30                 <td>{{ test_case.request_method }}</td>
31                 <td>{{ test_case.request_data }}</td>
32                 <td>{{ test_case.assert_key }}</td>
33                 <td>{{ test_case.extract_var }}</td>
34             </tr>
35             {% endfor %}
36             </tbody>
37         </table>
38 
39     </div>
40 </form>
41 {# 实现分页标签的代码 #}
42 {# 这里使用 bootstrap 渲染页面 #}
43 <div id="pages" class="text-center">
44     <nav>
45         <ul class="pagination">
46             <li class="step-links">
47                 {% if test_cases.has_previous %}
48                 <a class='active' href="?page={{ test_cases.previous_page_number }}">上一页</a>
49                 {% endif %}
50 
51                 <span class="current">
52                     第 {{ test_cases.number }} 页 / 共 {{ test_cases.paginator.num_pages }} 页</span>
53 
54                 {% if test_cases.has_next %}
55                 <a class='active' href="?page={{ test_cases.next_page_number }}">下一页</a>
56                 {% endif %}
57             </li>
58         </ul>
59     </nav>
60 </div>
61 {% endblock %}

4.7 模块页面展现所包含用例

1)新增模块页面的用例路由配置:

from django.urls import path, re_path
from . import views


urlpatterns = [
    path('', views.index),
    path('login/', views.login),
    path('logout/', views.logout),
    path('project/', views.project, name='project'),
    path('module/', views.module, name='module'),
    path('test_case/', views.test_case, name="test_case"),
    re_path('test_case_detail/(?P<test_case_id>[0-9]+)', views.test_case_detail, name="test_case_detail"),
    re_path('module_test_cases/(?P<module_id>[0-9]+)/$', views.module_test_cases, name="module_test_cases"),
]

2)新增视图函数:

  1 from django.shortcuts import render, redirect, HttpResponse
  2 from django.contrib import auth  # Django用户认证(Auth)组件通常用在用户的登陆注册上,用于判断当前的用户是否合法
  3 from django.contrib.auth.decorators import login_required
  4 from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage
  5 from .form import UserForm
  6 import traceback
  7 from .models import Project, Module, TestCase
  8 
  9 
 10 # 封装分页处理
 11 def get_paginator(request, data):
 12     paginator = Paginator(data, 10)  # 默认每页展现10条数据
 13     # 获取 url 后面的 page 参数的值, 首页不显示 page 参数, 默认值是 1
 14     page = request.GET.get('page')
 15     try:
 16         paginator_pages = paginator.page(page)
 17     except PageNotAnInteger:
 18         # 若是请求的页数不是整数, 返回第一页。
 19         paginator_pages = paginator.page(1)
 20     except InvalidPage:
 21         # 若是请求的页数不存在, 重定向页面
 22         return HttpResponse('找不到页面的内容')
 23     return paginator_pages
 24 
 25 
 26 # 项目菜单
 27 @login_required
 28 def project(request):
 29     print("request.user.is_authenticated: ", request.user.is_authenticated)
 30     projects = Project.objects.filter().order_by('-id')
 31     print("projects:", projects)
 32     return render(request, 'project.html', {'projects': get_paginator(request, projects)})
 33 
 34 
 35 # 模块菜单
 36 @login_required
 37 def module(request):
 38     if request.method == "GET":  # 请求get时候,id倒序查询全部的模块数据
 39         modules = Module.objects.filter().order_by('-id')
 40         return render(request, 'module.html', {'modules': get_paginator(request, modules)})
 41     else:  # 不然就是Post请求,会根据输入内容,使用模糊的方式查找全部的项目
 42         proj_name = request.POST['proj_name']
 43         projects = Project.objects.filter(name__contains=proj_name.strip())
 44         projs = [proj.id for proj in projects]
 45         modules = Module.objects.filter(belong_project__in=projs)  # 把项目中全部的模块都找出来
 46         return render(request, 'module.html', {'modules': get_paginator(request, modules), 'proj_name': proj_name})
 47 
 48 
 49 # 测试用例菜单
 50 @login_required
 51 def test_case(request):
 52     print("request.session['is_login']: {}".format(request.session['is_login']))
 53     test_cases = ""
 54     if request.method == "GET":
 55         test_cases = TestCase.objects.filter().order_by('id')
 56         print("testcases in testcase: {}".format(test_cases))
 57     elif request.method == "POST":
 58         print("request.POST: {}".format(request.POST))
 59         test_case_id_list = request.POST.getlist('testcases_list')
 60         if test_case_id_list:
 61             test_case_id_list.sort()
 62             print("test_case_id_list: {}".format(test_case_id_list))
 63         test_cases = TestCase.objects.filter().order_by('id')
 64     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
 65 
 66 
 67 # 用例详情页
 68 @login_required
 69 def test_case_detail(request, test_case_id):
 70     test_case_id = int(test_case_id)
 71     test_case = TestCase.objects.get(id=test_case_id)
 72     print("test_case: {}".format(test_case))
 73     print("test_case.id: {}".format(test_case.id))
 74     print("test_case.belong_project: {}".format(test_case.belong_project))
 75 
 76     return render(request, 'test_case_detail.html', {'test_case': test_case})
 77 
 78 
 79 # 模块页展现测试用例
 80 @login_required
 81 def module_test_cases(request, module_id):
 82     module = ""
 83     if module_id:  # 访问的时候,会从url中提取模块的id,根据模块id查询到模块数据,在模板中展示
 84         module = Module.objects.get(id=int(module_id))
 85     test_cases = TestCase.objects.filter(belong_module=module).order_by('-id')
 86     print("test_case in module_test_cases: {}".format(test_cases))
 87     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
 88 
 89 
 90 # 默认页的视图函数
 91 @login_required
 92 def index(request):
 93     return render(request, 'index.html')
 94 
 95 
 96 # 登陆页的视图函数
 97 def login(request):
 98     print("request.session.items(): {}".format(request.session.items()))  # 打印session信息
 99     if request.session.get('is_login', None):
100         return redirect('/')
101     # 若是是表单提交行为,则进行登陆校验
102     if request.method == "POST":
103         login_form = UserForm(request.POST)
104         message = "请检查填写的内容!"
105         if login_form.is_valid():
106             username = login_form.cleaned_data['username']
107             password = login_form.cleaned_data['password']
108             try:
109                 # 使用django提供的身份验证功能
110                 user = auth.authenticate(username=username, password=password)  # 从auth_user表中匹配信息,匹配到则返回用户对象
111                 if user is not None:
112                     print("用户【%s】登陆成功" % username)
113                     auth.login(request, user)
114                     request.session['is_login'] = True
115                     # 登陆成功,跳转主页
116                     return redirect('/')
117                 else:
118                     message = "用户名不存在或者密码不正确!"
119             except:
120                 traceback.print_exc()
121                 message = "登陆程序出现异常"
122         # 用户名或密码为空,返回登陆页和错误提示信息
123         else:
124             return render(request, 'login.html', locals())
125     # 不是表单提交,表明只是访问登陆页
126     else:
127         login_form = UserForm()
128         return render(request, 'login.html', locals())
129 
130 
131 # 注册页的视图函数
132 def register(request):
133     return render(request, 'register.html')
134 
135 
136 # 登出的视图函数:重定向至index视图函数
137 @login_required
138 def logout(request):
139     auth.logout(request)
140     request.session.flush()
141     return redirect("/login/")

3)修改模块的模板文件:在模块名称连接中,添加对应测试用例的路由地址。

 1 {% extends 'base.html' %}
 2 {% load static %}
 3 {% block title %}模块{% endblock %}
 4 
 5 {% block content %}
 6 <form action="{% url 'module'%}" method="POST">
 7     {% csrf_token %}
 8     <input style="margin-left: 5px;" type="text" name="proj_name" value="{{ proj_name }}" placeholder="输入项目名称搜索模块">
 9     <input type="submit" value="搜索">
10 </form>
11 
12 <div class="table-responsive">
13 
14     <table class="table table-striped">
15         <thead>
16         <tr>
17             <th>id</th>
18             <th>模块名称</th>
19             <th>所属项目</th>
20             <th>测试负责人</th>
21             <th>模块描述</th>
22             <th>建立时间</th>
23             <th>更新时间</th>
24             <th>测试结果统计</th>
25         </tr>
26         </thead>
27         <tbody>
28 
29         {% for module in modules %}
30         <tr>
31             <td>{{ module.id }}</td>
32             <td><a href="{% url 'module_test_cases' module.id %}">{{ module.name }}</a></td>
33             <td>{{ module.belong_project.name }}</td>
34             <td>{{ module.test_owner }}</td>
35             <td>{{ module.desc }}</td>
36             <td>{{ module.create_time|date:"Y-n-d H:i" }}</td>
37             <td>{{ module.update_time|date:"Y-n-d H:i" }}</td>
38             <td><a href="">查看</a></td>
39         </tr>
40         {% endfor %}
41 
42         </tbody>
43     </table>
44 </div>
45 
46 {# 实现分页标签的代码 #}
47 {# 这里使用 bootstrap 渲染页面 #}
48 <div id="pages" class="text-center">
49     <nav>
50         <ul class="pagination">
51             <li class="step-links">
52                 {% if modules.has_previous %}
53                 <a class='active' href="?page={{ modules.previous_page_number }}">上一页</a>
54                 {% endif %}
55 
56                 <span class="current">
57                     第 {{ modules.number }} 页 / 共 {{ modules.paginator.num_pages }} 页</span>
58 
59                 {% if modules.has_next %}
60                 <a class='active' href="?page={{ modules.next_page_number }}">下一页</a>
61                 {% endif %}
62             </li>
63         </ul>
64     </nav>
65 </div>
66 {% endblock %}

  

5. 用例集合

预期效果以下:

5.1 定义模型类

1)models.py 中新增 case_suite 模型类

 1 from django.db import models
 2 from smart_selects.db_fields import GroupedForeignKey  # pip install django-smart-selects:后台级联选择
 3 from django.contrib.auth.models import User
 4 
 5 
 6 class Project(models.Model):
 7     id = models.AutoField(primary_key=True)
 8     name = models.CharField('项目名称', max_length=50, unique=True, null=False)
 9     proj_owner = models.CharField('项目负责人', max_length=20, null=False)
10     test_owner = models.CharField('测试负责人', max_length=20, null=False)
11     dev_owner = models.CharField('开发负责人', max_length=20, null=False)
12     desc = models.CharField('项目描述', max_length=100, null=True)
13     create_time = models.DateTimeField('项目建立时间', auto_now_add=True)
14     update_time = models.DateTimeField('项目更新时间', auto_now=True, null=True)
15 
16     def __str__(self):
17         return self.name
18 
19     class Meta:
20         verbose_name = '项目信息表'
21         verbose_name_plural = '项目信息表'
22 
23 
24 class Module(models.Model):
25     id = models.AutoField(primary_key=True)
26     name = models.CharField('模块名称', max_length=50, null=False)
27     belong_project = models.ForeignKey(Project, on_delete=models.CASCADE)
28     test_owner = models.CharField('测试负责人', max_length=50, null=False)
29     desc = models.CharField('简要描述', max_length=100, null=True)
30     create_time = models.DateTimeField('建立时间', auto_now_add=True)
31     update_time = models.DateTimeField('更新时间', auto_now=True, null=True)
32 
33     def __str__(self):
34         return self.name
35 
36     class Meta:
37         verbose_name = '模块信息表'
38         verbose_name_plural = '模块信息表'
39 
40 
41 class TestCase(models.Model):
42     id = models.AutoField(primary_key=True)
43     case_name = models.CharField('用例名称', max_length=50, null=False)  # 如 register
44     belong_project = models.ForeignKey(Project, on_delete=models.CASCADE, verbose_name='所属项目')
45     belong_module = GroupedForeignKey(Module, "belong_project", on_delete=models.CASCADE, verbose_name='所属模块')
46     request_data = models.CharField('请求数据', max_length=1024, null=False, default='')
47     uri = models.CharField('接口地址', max_length=1024, null=False, default='')
48     assert_key = models.CharField('断言内容', max_length=1024, null=True)
49     maintainer = models.CharField('编写人员', max_length=1024, null=False, default='')
50     extract_var = models.CharField('提取变量表达式', max_length=1024, null=True)  # 示例:userid||userid": (\d+)
51     request_method = models.CharField('请求方式', max_length=1024, null=True)
52     status = models.IntegerField(null=True, help_text="0:表示有效,1:表示无效,用于软删除")
53     created_time = models.DateTimeField('建立时间', auto_now_add=True)
54     updated_time = models.DateTimeField('更新时间', auto_now=True, null=True)
55     user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='责任人', null=True)
56 
57     def __str__(self):
58         return self.case_name
59 
60     class Meta:
61         verbose_name = '测试用例表'
62         verbose_name_plural = '测试用例表'
63 
64 
65 class CaseSuite(models.Model):
66     id = models.AutoField(primary_key=True)
67     suite_desc = models.CharField('用例集合描述', max_length=100, blank=True, null=True)
68     if_execute = models.IntegerField(verbose_name='是否执行', null=False, default=0, help_text='0:执行;1:不执行')
69     test_case_model = models.CharField('测试执行模式', max_length=100, blank=True, null=True, help_text='data/keyword')
70     creator = models.CharField(max_length=50, blank=True, null=True)
71     create_time = models.DateTimeField('建立时间', auto_now=True)  # 建立时间-自动获取当前时间
72 
73     class Meta:
74         verbose_name = '用例集合表'
75         verbose_name_plural = '用例集合表'

2)数据迁移

在项目目录下,执行如下两个命令进行数据迁移(将模型类转换成数据库表):

python manage.py makemigrations  # 生成迁移文件(模型类的信息)
python manage.py migrate  # 执行开始迁移(将模型类信息转换成数据库表)

5.2 后台 admin 添加数据

1)注册模型类到 admin.py:

 1 from django.contrib import admin
 2 from .import models
 3 
 4 
 5 class ProjectAdmin(admin.ModelAdmin):
 6     list_display = ("id", "name", "proj_owner", "test_owner", "dev_owner", "desc", "create_time", "update_time")
 7 
 8 admin.site.register(models.Project, ProjectAdmin)
 9 
10 
11 class ModuleAdmin(admin.ModelAdmin):
12     list_display = ("id", "name", "belong_project", "test_owner", "desc", "create_time", "update_time")
13 
14 admin.site.register(models.Module, ModuleAdmin)
15 
16 
17 class TestCaseAdmin(admin.ModelAdmin):
18     list_display = (
19         "id", "case_name", "belong_project", "belong_module", "request_data", "uri", "assert_key", "maintainer",
20         "extract_var", "request_method", "status", "created_time", "updated_time", "user")
21 
22 admin.site.register(models.TestCase, TestCaseAdmin)
23 
24 
25 class CaseSuiteAdmin(admin.ModelAdmin):
26     list_display = ("id", "suite_desc", "creator", "create_time")
27 
28 admin.site.register(models.CaseSuite, CaseSuiteAdmin)

2)登陆 admin 系统,进入用例集合表,添加数据:

5.3 定义路由 

from django.urls import path, re_path
from . import views


urlpatterns = [
    path('', views.index),
    path('login/', views.login),
    path('logout/', views.logout),
    path('project/', views.project, name='project'),
    path('module/', views.module, name='module'),
    path('test_case/', views.test_case, name="test_case"),
    re_path('test_case_detail/(?P<test_case_id>[0-9]+)', views.test_case_detail, name="test_case_detail"),
    re_path('module_test_cases/(?P<module_id>[0-9]+)/$', views.module_test_cases, name="module_test_cases"),
    path('test_suite/', views.test_suite, name="test_suite"),
]

5.4 定义视图

  1 from django.shortcuts import render, redirect, HttpResponse
  2 from django.contrib import auth  # Django用户认证(Auth)组件通常用在用户的登陆注册上,用于判断当前的用户是否合法
  3 from django.contrib.auth.decorators import login_required
  4 from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage
  5 from .form import UserForm
  6 import traceback
  7 from .models import Project, Module, TestCase, CaseSuite
  8 
  9 
 10 # 封装分页处理
 11 def get_paginator(request, data):
 12     paginator = Paginator(data, 10)  # 默认每页展现10条数据
 13     # 获取 url 后面的 page 参数的值, 首页不显示 page 参数, 默认值是 1
 14     page = request.GET.get('page')
 15     try:
 16         paginator_pages = paginator.page(page)
 17     except PageNotAnInteger:
 18         # 若是请求的页数不是整数, 返回第一页。
 19         paginator_pages = paginator.page(1)
 20     except InvalidPage:
 21         # 若是请求的页数不存在, 重定向页面
 22         return HttpResponse('找不到页面的内容')
 23     return paginator_pages
 24 
 25 
 26 # 项目菜单
 27 @login_required
 28 def project(request):
 29     print("request.user.is_authenticated: ", request.user.is_authenticated)
 30     projects = Project.objects.filter().order_by('-id')
 31     print("projects:", projects)
 32     return render(request, 'project.html', {'projects': get_paginator(request, projects)})
 33 
 34 
 35 # 模块菜单
 36 @login_required
 37 def module(request):
 38     if request.method == "GET":  # 请求get时候,id倒序查询全部的模块数据
 39         modules = Module.objects.filter().order_by('-id')
 40         return render(request, 'module.html', {'modules': get_paginator(request, modules)})
 41     else:  # 不然就是Post请求,会根据输入内容,使用模糊的方式查找全部的项目
 42         proj_name = request.POST['proj_name']
 43         projects = Project.objects.filter(name__contains=proj_name.strip())
 44         projs = [proj.id for proj in projects]
 45         modules = Module.objects.filter(belong_project__in=projs)  # 把项目中全部的模块都找出来
 46         return render(request, 'module.html', {'modules': get_paginator(request, modules), 'proj_name': proj_name})
 47 
 48 
 49 # 测试用例菜单
 50 @login_required
 51 def test_case(request):
 52     print("request.session['is_login']: {}".format(request.session['is_login']))
 53     test_cases = ""
 54     if request.method == "GET":
 55         test_cases = TestCase.objects.filter().order_by('id')
 56         print("testcases in testcase: {}".format(test_cases))
 57     elif request.method == "POST":
 58         print("request.POST: {}".format(request.POST))
 59         test_case_id_list = request.POST.getlist('testcases_list')
 60         if test_case_id_list:
 61             test_case_id_list.sort()
 62             print("test_case_id_list: {}".format(test_case_id_list))
 63         test_cases = TestCase.objects.filter().order_by('id')
 64     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
 65 
 66 
 67 # 用例详情页
 68 @login_required
 69 def test_case_detail(request, test_case_id):
 70     test_case_id = int(test_case_id)
 71     test_case = TestCase.objects.get(id=test_case_id)
 72     print("test_case: {}".format(test_case))
 73     print("test_case.id: {}".format(test_case.id))
 74     print("test_case.belong_project: {}".format(test_case.belong_project))
 75 
 76     return render(request, 'test_case_detail.html', {'test_case': test_case})
 77 
 78 
 79 # 模块页展现测试用例
 80 @login_required
 81 def module_test_cases(request, module_id):
 82     module = ""
 83     if module_id:  # 访问的时候,会从url中提取模块的id,根据模块id查询到模块数据,在模板中展示
 84         module = Module.objects.get(id=int(module_id))
 85     test_cases = TestCase.objects.filter(belong_module=module).order_by('-id')
 86     print("test_case in module_test_cases: {}".format(test_cases))
 87     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
 88 
 89 
 90 @login_required
 91 def case_suite(request):
 92     case_suites = CaseSuite.objects.filter()
 93     return render(request, 'case_suite.html', {'case_suites': get_paginator(request, case_suites)})
 94 
 95 
 96 # 默认页的视图函数
 97 @login_required
 98 def index(request):
 99     return render(request, 'index.html')
100 
101 
102 # 登陆页的视图函数
103 def login(request):
104     print("request.session.items(): {}".format(request.session.items()))  # 打印session信息
105     if request.session.get('is_login', None):
106         return redirect('/')
107     # 若是是表单提交行为,则进行登陆校验
108     if request.method == "POST":
109         login_form = UserForm(request.POST)
110         message = "请检查填写的内容!"
111         if login_form.is_valid():
112             username = login_form.cleaned_data['username']
113             password = login_form.cleaned_data['password']
114             try:
115                 # 使用django提供的身份验证功能
116                 user = auth.authenticate(username=username, password=password)  # 从auth_user表中匹配信息,匹配到则返回用户对象
117                 if user is not None:
118                     print("用户【%s】登陆成功" % username)
119                     auth.login(request, user)
120                     request.session['is_login'] = True
121                     # 登陆成功,跳转主页
122                     return redirect('/')
123                 else:
124                     message = "用户名不存在或者密码不正确!"
125             except:
126                 traceback.print_exc()
127                 message = "登陆程序出现异常"
128         # 用户名或密码为空,返回登陆页和错误提示信息
129         else:
130             return render(request, 'login.html', locals())
131     # 不是表单提交,表明只是访问登陆页
132     else:
133         login_form = UserForm()
134         return render(request, 'login.html', locals())
135 
136 
137 # 注册页的视图函数
138 def register(request):
139     return render(request, 'register.html')
140 
141 
142 # 登出的视图函数:重定向至login视图函数
143 @login_required
144 def logout(request):
145     auth.logout(request)
146     request.session.flush()
147     return redirect("/login/")

5.5 定义模板

新增 templates/case_suite.html 模板:

 1 {% extends 'base.html' %}
 2 {% load static %}
 3 {% block title %}测试集合{% endblock %}
 4 {% block content %}
 5 <form action="" method="POST">
 6     {% csrf_token %}
 7 
 8     <div class="table-responsive">
 9         <table class="table table-striped">
10             <thead>
11             <tr>
12                 <th>id</th>
13                 <th>测试集合名称</th>
14                 <th>建立者</th>
15                 <th>建立时间</th>
16                 <th>查看/删除测试用例</th>
17                 <th>添加测试用例</th>
18                 <th>用例集合执行结果</th>
19             </tr>
20             </thead>
21             <tbody>
22 
23             {% for case_suite in case_suites %}
24             <tr>
25                 <td>{{ case_suite.id }}</td>
26                 <td>{{ case_suite.suite_desc }}</td>
27                 <td>{{ case_suite.creator }}</td>
28                 <td>{{ case_suite.create_time|date:"Y-n-d H:i" }}</td>
29                 <td><a href="">查看/删除测试用例</a></td>
30                 <td><a href="">添加测试用例</a></td>
31                 <td><a href="">查看用例集合执行结果</a></td>
32             </tr>
33             {% endfor %}
34             </tbody>
35         </table>
36     </div>
37 </form>
38 
39 {# 实现分页标签的代码 #}
40 {# 这里使用 bootstrap 渲染页面 #}
41 <div id="pages" class="text-center">
42     <nav>
43         <ul class="pagination">
44             <li class="step-links">
45                 {% if case_suites.has_previous %}
46                 <a class='active' href="?page={{ case_suites.previous_page_number }}">上一页</a>
47                 {% endif %}
48 
49                 <span class="current">
50                     第 {{ case_suites.number }} 页/共 {{ case_suites.paginator.num_pages }} 页</span>
51 
52                 {% if case_suites.has_next %}
53                 <a class='active' href="?page={{ case_suites.next_page_number }}">下一页</a>
54                 {% endif %}
55             </li>
56         </ul>
57     </nav>
58 </div>
59 {% endblock %}

2)修改 base 模板:菜单栏新增“用例集合”。

 1 <!DOCTYPE html>
 2 <html lang="zh-CN">
 3 {% load static %}
 4 <head>
 5     <meta charset="utf-8">
 6     <meta http-equiv="X-UA-Compatible" content="IE=edge">
 7     <meta name="viewport" content="width=device-width, initial-scale=1">
 8     <!-- 上述3个meta标签*必须*放在最前面,任何其余内容都*必须*跟随其后! -->
 9     <title>{% block title %}base{% endblock %}</title>
10 
11     <!-- Bootstrap -->
12     <link href="{% static 'bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
13 
14 
15     <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
16     <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
17     <!--[if lt IE 9]>
18     <script src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
19     <script src="https://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
20     <![endif]-->
21     {% block css %}{% endblock %}
22 </head>
23 <body>
24 <nav class="navbar navbar-default">
25     <div class="container-fluid">
26         <!-- Brand and toggle get grouped for better mobile display -->
27         <div class="navbar-header">
28             <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#my-nav"
29                     aria-expanded="false">
30                 <span class="sr-only">切换导航条</span>
31                 <span class="icon-bar"></span>
32                 <span class="icon-bar"></span>
33                 <span class="icon-bar"></span>
34             </button>
35             <a class="navbar-brand" href="/">自动化测试平台</a>
36         </div>
37 
38         <div class="collapse navbar-collapse" id="my-nav">
39             <ul class="nav navbar-nav">
40                 <li class="active"><a href="/project/">项目</a></li>
41                 <li class="active"><a href="/module/">模块</a></li>
42                 <li class="active"><a href="/test_case/">测试用例</a></li>
43                 <li class="active"><a href="/case_suite/">用例集合</a></li>
44             </ul>
45             <ul class="nav navbar-nav navbar-right">
46                 {% if request.user.is_authenticated %}
47                 <li><a href="#">当前在线:{{ request.user.username }}</a></li>
48                 <li><a href="/logout">登出</a></li>
49                 {% else %}
50                 <li><a href="/login">登陆</a></li>
51 
52                 {% endif %}
53             </ul>
54         </div><!-- /.navbar-collapse -->
55     </div><!-- /.container-fluid -->
56 </nav>
57 
58 {% block content %}{% endblock %}
59 
60 
61 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
62 <script src="{% static 'js/jquery-3.4.1.js' %}"></script>
63 <!-- Include all compiled plugins (below), or include individual files as needed -->
64 <script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script>
65 </body>
66 </html>

 

6. 用例集合添加测试用例

预期效果以下:

6.1 定义模型类

1)在 models.py 中,增长模型类 SuiteCase,记录用例集合所关联的用例。

 1 from django.db import models
 2 from smart_selects.db_fields import GroupedForeignKey  # pip install django-smart-selects:后台级联选择
 3 from django.contrib.auth.models import User
 4 
 5 
 6 class Project(models.Model):
 7     id = models.AutoField(primary_key=True)
 8     name = models.CharField('项目名称', max_length=50, unique=True, null=False)
 9     proj_owner = models.CharField('项目负责人', max_length=20, null=False)
10     test_owner = models.CharField('测试负责人', max_length=20, null=False)
11     dev_owner = models.CharField('开发负责人', max_length=20, null=False)
12     desc = models.CharField('项目描述', max_length=100, null=True)
13     create_time = models.DateTimeField('项目建立时间', auto_now_add=True)
14     update_time = models.DateTimeField('项目更新时间', auto_now=True, null=True)
15 
16     def __str__(self):
17         return self.name
18 
19     class Meta:
20         verbose_name = '项目信息表'
21         verbose_name_plural = '项目信息表'
22 
23 
24 class Module(models.Model):
25     id = models.AutoField(primary_key=True)
26     name = models.CharField('模块名称', max_length=50, null=False)
27     belong_project = models.ForeignKey(Project, on_delete=models.CASCADE)
28     test_owner = models.CharField('测试负责人', max_length=50, null=False)
29     desc = models.CharField('简要描述', max_length=100, null=True)
30     create_time = models.DateTimeField('建立时间', auto_now_add=True)
31     update_time = models.DateTimeField('更新时间', auto_now=True, null=True)
32 
33     def __str__(self):
34         return self.name
35 
36     class Meta:
37         verbose_name = '模块信息表'
38         verbose_name_plural = '模块信息表'
39 
40 
41 class TestCase(models.Model):
42     id = models.AutoField(primary_key=True)
43     case_name = models.CharField('用例名称', max_length=50, null=False)  # 如 register
44     belong_project = models.ForeignKey(Project, on_delete=models.CASCADE, verbose_name='所属项目')
45     belong_module = GroupedForeignKey(Module, "belong_project", on_delete=models.CASCADE, verbose_name='所属模块')
46     request_data = models.CharField('请求数据', max_length=1024, null=False, default='')
47     uri = models.CharField('接口地址', max_length=1024, null=False, default='')
48     assert_key = models.CharField('断言内容', max_length=1024, null=True)
49     maintainer = models.CharField('编写人员', max_length=1024, null=False, default='')
50     extract_var = models.CharField('提取变量表达式', max_length=1024, null=True)  # 示例:userid||userid": (\d+)
51     request_method = models.CharField('请求方式', max_length=1024, null=True)
52     status = models.IntegerField(null=True, help_text="0:表示有效,1:表示无效,用于软删除")
53     created_time = models.DateTimeField('建立时间', auto_now_add=True)
54     updated_time = models.DateTimeField('更新时间', auto_now=True, null=True)
55     user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='责任人', null=True)
56 
57     def __str__(self):
58         return self.case_name
59 
60     class Meta:
61         verbose_name = '测试用例表'
62         verbose_name_plural = '测试用例表'
63 
64 
65 class CaseSuite(models.Model):
66     id = models.AutoField(primary_key=True)
67     suite_desc = models.CharField('用例集合描述', max_length=100, blank=True, null=True)
68     if_execute = models.IntegerField(verbose_name='是否执行', null=False, default=0, help_text='0:执行;1:不执行')
69     test_case_model = models.CharField('测试执行模式', max_length=100, blank=True, null=True, help_text='data/keyword')
70     creator = models.CharField(max_length=50, blank=True, null=True)
71     create_time = models.DateTimeField('建立时间', auto_now=True)  # 建立时间-自动获取当前时间
72 
73     class Meta:
74         verbose_name = "用例集合表"
75         verbose_name_plural = '用例集合表'
76 
77 
78 class SuiteCase(models.Model):
79     id = models.AutoField(primary_key=True)
80     case_suite = models.ForeignKey(CaseSuite, on_delete=models.CASCADE, verbose_name='用例集合')
81     test_case = models.ForeignKey(TestCase, on_delete=models.CASCADE, verbose_name='测试用例')
82     status = models.IntegerField(verbose_name='是否有效', null=False, default=1, help_text='0:有效,1:无效')
83     create_time = models.DateTimeField('建立时间', auto_now=True)  # 建立时间-自动获取当前时间

2)数据迁移

在项目目录下,执行如下两个命令进行数据迁移(将模型类转换成数据库表):

python manage.py makemigrations  # 生成迁移文件(模型类的信息)
python manage.py migrate  # 执行开始迁移(将模型类信息转换成数据库表)

6.2 定义路由 

from django.urls import path, re_path
from . import views


urlpatterns = [
    path('', views.index),
    path('login/', views.login),
    path('logout/', views.logout),
    path('project/', views.project, name='project'),
    path('module/', views.module, name='module'),
    path('test_case/', views.test_case, name="test_case"),
    re_path('test_case_detail/(?P<test_case_id>[0-9]+)', views.test_case_detail, name="test_case_detail"),
    re_path('module_test_cases/(?P<module_id>[0-9]+)/$', views.module_test_cases, name="module_test_cases"),
    path('case_suite/', views.case_suite, name="case_suite"),
    re_path('add_case_in_suite/(?P<suite_id>[0-9]+)', views.add_case_in_suite, name="add_case_in_suite"),
]

6.3 定义视图

  1 from django.shortcuts import render, redirect, HttpResponse
  2 from django.contrib import auth  # Django用户认证(Auth)组件通常用在用户的登陆注册上,用于判断当前的用户是否合法
  3 from django.contrib.auth.decorators import login_required
  4 from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage
  5 from .form import UserForm
  6 import traceback
  7 from .models import Project, Module, TestCase, CaseSuite, SuiteCase
  8 
  9 
 10 # 封装分页处理
 11 def get_paginator(request, data):
 12     paginator = Paginator(data, 10)  # 默认每页展现10条数据
 13     # 获取 url 后面的 page 参数的值, 首页不显示 page 参数, 默认值是 1
 14     page = request.GET.get('page')
 15     try:
 16         paginator_pages = paginator.page(page)
 17     except PageNotAnInteger:
 18         # 若是请求的页数不是整数, 返回第一页。
 19         paginator_pages = paginator.page(1)
 20     except InvalidPage:
 21         # 若是请求的页数不存在, 重定向页面
 22         return HttpResponse('找不到页面的内容')
 23     return paginator_pages
 24 
 25 
 26 # 项目菜单
 27 @login_required
 28 def project(request):
 29     print("request.user.is_authenticated: ", request.user.is_authenticated)
 30     projects = Project.objects.filter().order_by('-id')
 31     print("projects:", projects)
 32     return render(request, 'project.html', {'projects': get_paginator(request, projects)})
 33 
 34 
 35 # 模块菜单
 36 @login_required
 37 def module(request):
 38     if request.method == "GET":  # 请求get时候,id倒序查询全部的模块数据
 39         modules = Module.objects.filter().order_by('-id')
 40         return render(request, 'module.html', {'modules': get_paginator(request, modules)})
 41     else:  # 不然就是Post请求,会根据输入内容,使用模糊的方式查找全部的项目
 42         proj_name = request.POST['proj_name']
 43         projects = Project.objects.filter(name__contains=proj_name.strip())
 44         projs = [proj.id for proj in projects]
 45         modules = Module.objects.filter(belong_project__in=projs)  # 把项目中全部的模块都找出来
 46         return render(request, 'module.html', {'modules': get_paginator(request, modules), 'proj_name': proj_name})
 47 
 48 
 49 # 测试用例页
 50 @login_required
 51 def test_case(request):
 52     print("request.session['is_login']: {}".format(request.session['is_login']))
 53     test_cases = ""
 54     if request.method == "GET":
 55         test_cases = TestCase.objects.filter().order_by('id')
 56         print("testcases in testcase: {}".format(test_cases))
 57     elif request.method == "POST":
 58         print("request.POST: {}".format(request.POST))
 59         test_case_id_list = request.POST.getlist('testcases_list')
 60         if test_case_id_list:
 61             test_case_id_list.sort()
 62             print("test_case_id_list: {}".format(test_case_id_list))
 63         test_cases = TestCase.objects.filter().order_by('id')
 64     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
 65 
 66 
 67 # 用例详情页
 68 @login_required
 69 def test_case_detail(request, test_case_id):
 70     test_case_id = int(test_case_id)
 71     test_case = TestCase.objects.get(id=test_case_id)
 72     print("test_case: {}".format(test_case))
 73     print("test_case.id: {}".format(test_case.id))
 74     print("test_case.belong_project: {}".format(test_case.belong_project))
 75 
 76     return render(request, 'test_case_detail.html', {'test_case': test_case})
 77 
 78 
 79 # 模块页展现测试用例
 80 @login_required
 81 def module_test_cases(request, module_id):
 82     module = ""
 83     if module_id:  # 访问的时候,会从url中提取模块的id,根据模块id查询到模块数据,在模板中展示
 84         module = Module.objects.get(id=int(module_id))
 85     test_cases = TestCase.objects.filter(belong_module=module).order_by('-id')
 86     print("test_case in module_test_cases: {}".format(test_cases))
 87     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
 88 
 89 
 90 # 用例集合页
 91 @login_required
 92 def case_suite(request):
 93     case_suites = CaseSuite.objects.filter()
 94     return render(request, 'case_suite.html', {'case_suites': get_paginator(request, case_suites)})
 95 
 96 
 97 # 用例集合-添加测试用例页
 98 @login_required
 99 def add_case_in_suite(request, suite_id):
100     # 查询指定的用例集合
101     case_suite = CaseSuite.objects.get(id=suite_id)
102     # 根据id号查询全部的用例
103     test_cases = TestCase.objects.filter().order_by('id')
104     if request.method == "GET":
105         print("test cases:", test_cases)
106     elif request.method == "POST":
107         test_cases_list = request.POST.getlist('testcases_list')
108         # 若是页面勾选了用例
109         if test_cases_list:
110             print("勾选用例id:", test_cases_list)
111             # 根据页面勾选的用例与查询出的全部用例一一比较
112             for test_case in test_cases_list:
113                 test_case = TestCase.objects.get(id=int(test_case))
114                 # 匹配成功则添加用例
115                 SuiteCase.objects.create(case_suite=case_suite, test_case=test_case)
116         # 未勾选用例
117         else:
118             print("添加测试用例失败")
119             return HttpResponse("添加的测试用例为空,请选择用例后再添加!")
120     return render(request, 'add_case_in_suite.html',
121           {'test_cases': get_paginator(request, test_cases), 'case_suite': case_suite})
122 
123 
124 # 默认页的视图函数
125 @login_required
126 def index(request):
127     return render(request, 'index.html')
128 
129 
130 # 登陆页的视图函数
131 def login(request):
132     print("request.session.items(): {}".format(request.session.items()))  # 打印session信息
133     if request.session.get('is_login', None):
134         return redirect('/')
135     # 若是是表单提交行为,则进行登陆校验
136     if request.method == "POST":
137         login_form = UserForm(request.POST)
138         message = "请检查填写的内容!"
139         if login_form.is_valid():
140             username = login_form.cleaned_data['username']
141             password = login_form.cleaned_data['password']
142             try:
143                 # 使用django提供的身份验证功能
144                 user = auth.authenticate(username=username, password=password)  # 从auth_user表中匹配信息,匹配到则返回用户对象
145                 if user is not None:
146                     print("用户【%s】登陆成功" % username)
147                     auth.login(request, user)
148                     request.session['is_login'] = True
149                     # 登陆成功,跳转主页
150                     return redirect('/')
151                 else:
152                     message = "用户名不存在或者密码不正确!"
153             except:
154                 traceback.print_exc()
155                 message = "登陆程序出现异常"
156         # 用户名或密码为空,返回登陆页和错误提示信息
157         else:
158             return render(request, 'login.html', locals())
159     # 不是表单提交,表明只是访问登陆页
160     else:
161         login_form = UserForm()
162         return render(request, 'login.html', locals())
163 
164 
165 # 注册页的视图函数
166 def register(request):
167     return render(request, 'register.html')
168 
169 
170 # 登出的视图函数:重定向至login视图函数
171 @login_required
172 def logout(request):
173     auth.logout(request)
174     request.session.flush()
175     return redirect("/login/")

6.4 定义模板文件

1)新增添加测试用例页的模板文件 templates/add_case_in_suite.html:

 1 {% extends 'base.html' %}
 2 {% load static %}
 3 {% block title %}管理测试集合{% endblock %}
 4 {% block content %}
 5 
 6 <script type="text/javascript">
 7     //页面加载的时候,全部的复选框都是未选中的状态
 8     function checkOrCancelAll() {
 9         var all_check = document.getElementById("all_check");//1.获取all的元素对象
10         var all_check = all_check.checked;//2.获取选中状态
11         var allCheck = document.getElementsByName("testcases_list");//3.若checked=true,将全部的复选框选中,checked=false,将全部的复选框取消
12         //4.循环遍历取出每个复选框中的元素
13         if (all_check)//全选
14         {
15             for (var i = 0; i < allCheck.length; i++) {
16                 //设置复选框的选中状态
17                 allCheck[i].checked = true;
18             }
19         } else//取消全选
20         {
21             for (var i = 0; i < allCheck.length; i++) {
22                 allCheck[i].checked = false;
23             }
24         }
25     }
26 
27     function ischecked() {
28         var allCheck = document.getElementsByName("testcases_list");//3.若checked=true,将全部的复选框选中,checked=false,将全部的复选框取消
29         for (var i = 0; i < allCheck.length; i++) {
30             if (allCheck[i].checked == true) {
31                 alert("成功添加所选测试用例至测试集合【{{case_suite.suite_desc}}】");
32                 return true
33             }
34         }
35         alert("请选择要添加的测试用例!")
36         return false
37     }
38 
39 
40 
41 </script>
42 <form action="" method="POST">
43     {% csrf_token %}
44     <input type="submit" id="all_check1" value='添加测试用例' onclick="return ischecked()"/>
45     <div class="table-responsive">
46         <table class="table table-striped">
47             <thead>
48             <tr>
49                 <th><input type="checkbox" id="all_check" onclick="checkOrCancelAll();"/>id</th>
50                 <th>用例名称</th>
51                 <th>所属项目</th>
52                 <th>所属模块</th>
53                 <th>编写人员</th>
54                 <th>建立时间</th>
55                 <th>更新时间</th>
56                 <th>建立用例用户名</th>
57             </tr>
58             </thead>
59             <tbody>
60             {% for test_case in test_cases %}
61             <tr>
62                 <td><input type="checkbox" value="{{ test_case.id }}" name="testcases_list"> {{ test_case.id }}</td>
63                 <td><a href="{% url 'test_case_detail' test_case.id%}">{{ test_case.case_name }}</a></td>
64                 <td>{{ test_case.belong_project.name }}</td>
65                 <td>{{ test_case.belong_module.name }}</td>
66                 <td>{{ test_case.maintainer }}</td>
67                 <td>{{ test_case.created_time|date:"Y-n-d H:i" }}</td>
68                 <td>{{ test_case.updated_time|date:"Y-n-d H:i" }}</td>
69                 <td>{{ test_case.user.username }}</td>
70             </tr>
71             {% endfor %}
72             </tbody>
73         </table>
74     </div>
75 </form>
76 {# 实现分页标签的代码 #}
77 {# 这里使用 bootstrap 渲染页面 #}
78 <div id="pages" class="text-center">
79     <nav>
80         <ul class="pagination">
81             <li class="step-links">
82                 {% if test_cases.has_previous %}
83                 <a class='active' href="?page={{ test_cases.previous_page_number }}">上一页</a>
84                 {% endif %}
85                 <span class="current">
86                     第 {{ test_cases.number }} 页 / 共 {{ test_cases.paginator.num_pages }} 页</span>
87                 {% if test_cases.has_next %}
88                 <a class='active' href="?page={{ test_cases.next_page_number }}">下一页</a>
89                 {% endif %}
90             </li>
91         </ul>
92     </nav>
93 </div>
94 {% endblock %}

2)修改用例集合模板文件 templates/case_suite.html:修改“添加测试用例”的连接地址。

 1 {% extends 'base.html' %}
 2 {% load static %}
 3 {% block title %}用例集合{% endblock %}
 4 {% block content %}
 5 <form action="" method="POST">
 6     {% csrf_token %}
 7 
 8     <div class="table-responsive">
 9         <table class="table table-striped">
10             <thead>
11             <tr>
12                 <th>id</th>
13                 <th>测试集合名称</th>
14                 <th>建立者</th>
15                 <th>建立时间</th>
16                 <th>查看/删除测试用例</th>
17                 <th>添加测试用例</th>
18                 <th>用例集合执行结果</th>
19             </tr>
20             </thead>
21             <tbody>
22 
23             {% for case_suite in case_suites %}
24             <tr>
25                 <td>{{ case_suite.id }}</td>
26                 <td>{{ case_suite.suite_desc }}</td>
27                 <td>{{ case_suite.creator }}</td>
28                 <td>{{ case_suite.create_time|date:"Y-n-d H:i" }}</td>
29                 <td><a href="">查看/删除测试用例</a></td>
30                 <td><a href="{% url 'add_case_in_suite' case_suite.id %}">添加测试用例</a></td>
31                 <td><a href="">查看用例集合执行结果</a></td>
32             </tr>
33             {% endfor %}
34             </tbody>
35         </table>
36     </div>
37 </form>
38 
39 {# 实现分页标签的代码 #}
40 {# 这里使用 bootstrap 渲染页面 #}
41 <div id="pages" class="text-center">
42     <nav>
43         <ul class="pagination">
44             <li class="step-links">
45                 {% if case_suites.has_previous %}
46                 <a class='active' href="?page={{ case_suites.previous_page_number }}">上一页</a>
47                 {% endif %}
48 
49                 <span class="current">
50                     第 {{ case_suites.number }} 页 / 共 {{ case_suites.paginator.num_pages }} 页</span>
51 
52                 {% if case_suites.has_next %}
53                 <a class='active' href="?page={{ case_suites.next_page_number }}">下一页</a>
54                 {% endif %}
55             </li>
56         </ul>
57     </nav>
58 </div>
59 {% endblock %}

 

7. 用例集合查看/删除测试用例

预期效果以下:

 7.1 定义路由

from django.urls import path, re_path
from . import views


urlpatterns = [
    path('', views.index),
    path('login/', views.login),
    path('logout/', views.logout),
    path('project/', views.project, name='project'),
    path('module/', views.module, name='module'),
    path('test_case/', views.test_case, name="test_case"),
    re_path('test_case_detail/(?P<test_case_id>[0-9]+)', views.test_case_detail, name="test_case_detail"),
    re_path('module_test_cases/(?P<module_id>[0-9]+)/$', views.module_test_cases, name="module_test_cases"),
    path('case_suite/', views.case_suite, name="case_suite"),
    re_path('add_case_in_suite/(?P<suite_id>[0-9]+)', views.add_case_in_suite, name="add_case_in_suite"),
    re_path('show_and_delete_case_in_suite/(?P<suite_id>[0-9]+)', views.show_and_delete_case_in_suite, name="show_and_delete_case_in_suite"),
]

7.2 定义视图函数 

  1 from django.shortcuts import render, redirect, HttpResponse
  2 from django.contrib import auth  # Django用户认证(Auth)组件通常用在用户的登陆注册上,用于判断当前的用户是否合法
  3 from django.contrib.auth.decorators import login_required
  4 from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage
  5 from .form import UserForm
  6 import traceback
  7 from .models import Project, Module, TestCase, CaseSuite, SuiteCase, InterfaceServer
  8 from .task import case_task
  9 
 10 
 11 # 封装分页处理
 12 def get_paginator(request, data):
 13     paginator = Paginator(data, 10)  # 默认每页展现10条数据
 14     # 获取 url 后面的 page 参数的值, 首页不显示 page 参数, 默认值是 1
 15     page = request.GET.get('page')
 16     try:
 17         paginator_pages = paginator.page(page)
 18     except PageNotAnInteger:
 19         # 若是请求的页数不是整数, 返回第一页。
 20         paginator_pages = paginator.page(1)
 21     except InvalidPage:
 22         # 若是请求的页数不存在, 重定向页面
 23         return HttpResponse('找不到页面的内容')
 24     return paginator_pages
 25 
 26 
 27 # 项目页
 28 @login_required
 29 def project(request):
 30     print("request.user.is_authenticated: ", request.user.is_authenticated)
 31     projects = Project.objects.filter().order_by('-id')
 32     print("projects:", projects)
 33     return render(request, 'project.html', {'projects': get_paginator(request, projects)})
 34 
 35 
 36 # 模块页
 37 @login_required
 38 def module(request):
 39     if request.method == "GET":  # 请求get时候,id倒序查询全部的模块数据
 40         modules = Module.objects.filter().order_by('-id')
 41         return render(request, 'module.html', {'modules': get_paginator(request, modules)})
 42     else:  # 不然就是Post请求,会根据输入内容,使用模糊的方式查找全部的项目
 43         proj_name = request.POST['proj_name']
 44         projects = Project.objects.filter(name__contains=proj_name.strip())
 45         projs = [proj.id for proj in projects]
 46         modules = Module.objects.filter(belong_project__in=projs)  # 把项目中全部的模块都找出来
 47         return render(request, 'module.html', {'modules': get_paginator(request, modules), 'proj_name': proj_name})
 48 
 49 
 50 # 获取测试用例执行的接口地址
 51 def get_server_address(env):
 52     if env:  # 环境处理
 53         env_data = InterfaceServer.objects.filter(env=env[0])
 54         print("env_data: {}".format(env_data))
 55         if env_data:
 56             ip = env_data[0].ip
 57             port = env_data[0].port
 58             print("ip: {}, port: {}".format(ip, port))
 59             server_address = "http://{}:{}".format(ip, port)
 60             print("server_address: {}".format(server_address))
 61             return server_address
 62         else:
 63             return ""
 64     else:
 65         return ""
 66 
 67 
 68 # 测试用例页
 69 @login_required
 70 def test_case(request):
 71     print("request.session['is_login']: {}".format(request.session['is_login']))
 72     test_cases = ""
 73     if request.method == "GET":
 74         test_cases = TestCase.objects.filter().order_by('id')
 75         print("testcases in testcase: {}".format(test_cases))
 76     elif request.method == "POST":
 77         print("request.POST: {}".format(request.POST))
 78         test_case_id_list = request.POST.getlist('testcases_list')
 79         if test_case_id_list:
 80             test_case_id_list.sort()
 81             print("test_case_id_list: {}".format(test_case_id_list))
 82         test_cases = TestCase.objects.filter().order_by('id')
 83     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
 84 
 85 
 86 # 用例详情页
 87 @login_required
 88 def test_case_detail(request, test_case_id):
 89     test_case_id = int(test_case_id)
 90     test_case = TestCase.objects.get(id=test_case_id)
 91     print("test_case: {}".format(test_case))
 92     print("test_case.id: {}".format(test_case.id))
 93     print("test_case.belong_project: {}".format(test_case.belong_project))
 94 
 95     return render(request, 'test_case_detail.html', {'test_case': test_case})
 96 
 97 
 98 # 模块页展现测试用例
 99 @login_required
100 def module_test_cases(request, module_id):
101     module = ""
102     if module_id:  # 访问的时候,会从url中提取模块的id,根据模块id查询到模块数据,在模板中展示
103         module = Module.objects.get(id=int(module_id))
104     test_cases = TestCase.objects.filter(belong_module=module).order_by('-id')
105     print("test_case in module_test_cases: {}".format(test_cases))
106     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
107 
108 
109 # 用例集合页
110 @login_required
111 def case_suite(request):
112     case_suites = CaseSuite.objects.filter()
113     return render(request, 'case_suite.html', {'case_suites': get_paginator(request, case_suites)})
114 
115 
116 # 用例集合-添加测试用例页
117 @login_required
118 def add_case_in_suite(request, suite_id):
119     # 查询指定的用例集合
120     case_suite = CaseSuite.objects.get(id=suite_id)
121     # 根据id号查询全部的用例
122     test_cases = TestCase.objects.filter().order_by('id')
123     if request.method == "GET":
124         print("test cases:", test_cases)
125     elif request.method == "POST":
126         test_cases_list = request.POST.getlist('testcases_list')
127         # 若是页面勾选了用例
128         if test_cases_list:
129             print("勾选用例id:", test_cases_list)
130             # 根据页面勾选的用例与查询出的全部用例一一比较
131             for test_case in test_cases_list:
132                 test_case = TestCase.objects.get(id=int(test_case))
133                 # 匹配成功则添加用例
134                 SuiteCase.objects.create(case_suite=case_suite, test_case=test_case)
135         # 未勾选用例
136         else:
137             print("添加测试用例失败")
138             return HttpResponse("添加的测试用例为空,请选择用例后再添加!")
139     return render(request, 'add_case_in_suite.html',
140           {'test_cases': get_paginator(request, test_cases), 'case_suite': case_suite})
141 
142 
143 # 用例集合页-查看/删除用例
144 @login_required
145 def show_and_delete_case_in_suite(request, suite_id):
146     case_suite = CaseSuite.objects.get(id=suite_id)
147     test_cases = SuiteCase.objects.filter(case_suite=case_suite)
148     if request.method == "POST":
149         test_cases_list = request.POST.getlist('test_cases_list')
150         if test_cases_list:
151             print("勾选用例:", test_cases_list)
152             for test_case in test_cases_list:
153                 test_case = TestCase.objects.get(id=int(test_case))
154                 SuiteCase.objects.filter(case_suite=case_suite, test_case=test_case).first().delete()
155         else:
156             print("测试用例删除失败")
157             return HttpResponse("所选测试用例为空,请选择用例后再进行删除!")
158     case_suite = CaseSuite.objects.get(id=suite_id)
159     return render(request, 'show_and_delete_case_in_suite.html',
160                   {'test_cases': get_paginator(request, test_cases), 'case_suite': case_suite})
161 
162 
163 # 默认页的视图函数
164 @login_required
165 def index(request):
166     return render(request, 'index.html')
167 
168 
169 # 登陆页的视图函数
170 def login(request):
171     print("request.session.items(): {}".format(request.session.items()))  # 打印session信息
172     if request.session.get('is_login', None):
173         return redirect('/')
174     # 若是是表单提交行为,则进行登陆校验
175     if request.method == "POST":
176         login_form = UserForm(request.POST)
177         message = "请检查填写的内容!"
178         if login_form.is_valid():
179             username = login_form.cleaned_data['username']
180             password = login_form.cleaned_data['password']
181             try:
182                 # 使用django提供的身份验证功能
183                 user = auth.authenticate(username=username, password=password)  # 从auth_user表中匹配信息,匹配到则返回用户对象
184                 if user is not None:
185                     print("用户【%s】登陆成功" % username)
186                     auth.login(request, user)
187                     request.session['is_login'] = True
188                     # 登陆成功,跳转主页
189                     return redirect('/')
190                 else:
191                     message = "用户名不存在或者密码不正确!"
192             except:
193                 traceback.print_exc()
194                 message = "登陆程序出现异常"
195         # 用户名或密码为空,返回登陆页和错误提示信息
196         else:
197             return render(request, 'login.html', locals())
198     # 不是表单提交,表明只是访问登陆页
199     else:
200         login_form = UserForm()
201         return render(request, 'login.html', locals())
202 
203 
204 # 注册页的视图函数
205 def register(request):
206     return render(request, 'register.html')
207 
208 
209 # 登出的视图函数:重定向至login视图函数
210 @login_required
211 def logout(request):
212     auth.logout(request)
213     request.session.flush()
214     return redirect("/login/")

7.2 定义模板文件

1)新建 templates/show_and_delete_case_in_suite.html:

  1 {% extends 'base.html' %}
  2 {% load static %}
  3 {% block title %}查看/删除测试用例{% endblock %}
  4 {% block content %}
  5 
  6 <script type="text/javascript">
  7     //页面加载的时候,全部的复选框都是未选中的状态
  8     function checkOrCancelAll() {
  9         var all_check = document.getElementById("all_check");//1.获取all的元素对象
 10         var all_check = all_check.checked;//2.获取选中状态
 11         var allCheck = document.getElementsByName("test_cases_list");//3.若checked=true,将全部的复选框选中,checked=false,将全部的复选框取消
 12         //4.循环遍历取出每个复选框中的元素
 13         if (all_check)//全选
 14         {
 15 
 16             for (var i = 0; i < allCheck.length; i++) {
 17                 //设置复选框的选中状态
 18                 allCheck[i].checked = true;
 19             }
 20 
 21         } else//取消全选
 22         {
 23             for (var i = 0; i < allCheck.length; i++) {
 24                 allCheck[i].checked = false;
 25             }
 26         }
 27     }
 28 
 29     function ischecked() {
 30 
 31         var allCheck = document.getElementsByName("test_cases_list");//3.若checked=true,将全部的复选框选中,checked=false,将全部的复选框取消
 32         for (var i = 0; i < allCheck.length; i++) {
 33 
 34             if (allCheck[i].checked == true) {
 35                 alert("所选用例删除成功!");
 36                 return true
 37             }
 38         }
 39         alert("请选择要删除的测试用例!")
 40         return false
 41     }
 42 
 43 
 44 </script>
 45 
 46 <div><p style="margin-left: 5px;">测试集合名称:<b>{{case_suite.suite_desc}}</b></p>
 47     <div>
 48         <form action="" method="POST">
 49             {% csrf_token %}
 50             <input style="margin-left: 5px;" type="submit" id="all_check1" value='删除测试集合用例' onclick="return ischecked()"/>
 51             <div class="table-responsive">
 52                 <table class="table table-striped">
 53                     <thead>
 54                     <tr>
 55                         <th width="4%"><input type="checkbox" id="all_check" onclick="checkOrCancelAll();"/>全选</th>
 56                         <th width="6%">用例序号</th>
 57                         <th>用例名称</th>
 58                         <th>所属项目</th>
 59                         <th>所属模块</th>
 60                         <th>编写人员</th>
 61                         <th>建立时间</th>
 62                         <th>更新时间</th>
 63                         <th>建立用例用户名</th>
 64                     </tr>
 65                     </thead>
 66                     <tbody>
 67 
 68                     {% for test_case in test_cases %}
 69                     <tr>
 70                         <td><input type="checkbox" value="{{ test_case.test_case.id }}" name="test_cases_list"></td>
 71                         <td>{{ test_case.test_case.id }}</td>
 72                         <td><a href="{% url 'test_case_detail' test_case.test_case.id%}">{{ test_case.test_case.case_name }}</a></td>
 73                         <td>{{ test_case.test_case.belong_project.name }}</td>
 74                         <td>{{ test_case.test_case.belong_module.name }}</td>
 75                         <td>{{ test_case.test_case.maintainer }}</td>
 76                         <td>{{ test_case.test_case.created_time|date:"Y-n-d H:i" }}</td>
 77                         <td>{{ test_case.test_case.updated_time|date:"Y-n-d H:i" }}</td>
 78                         <td>{{ test_case.test_case.user.username }}</td>
 79                     </tr>
 80                     {% endfor %}
 81                     </tbody>
 82                 </table>
 83             </div>
 84         </form>
 85 
 86         {# 实现分页标签的代码 #}
 87         {# 这里使用 bootstrap 渲染页面 #}
 88         <div id="pages" class="text-center">
 89             <nav>
 90                 <ul class="pagination">
 91                     <li class="step-links">
 92                         {% if test_cases.has_previous %}
 93                         <a class='active' href="?page={{ test_cases.previous_page_number }}">上一页</a>
 94                         {% endif %}
 95 
 96                         <span class="current">
 97                     第 {{ test_cases.number }} 页 / 共 {{ test_cases.paginator.num_pages }} 页</span>
 98 
 99                         {% if test_cases.has_next %}
100                         <a class='active' href="?page={{ test_cases.next_page_number }}">下一页</a>
101                         {% endif %}
102                     </li>
103                 </ul>
104             </nav>
105         </div>
106     </div>
107 </div>
108 {% endblock %}

2)修改 templates/case_suite.html:增长“查看/删除测试用例”连接

 1 {% extends 'base.html' %}
 2 {% load static %}
 3 {% block title %}用例集合{% endblock %}
 4 {% block content %}
 5 <form action="" method="POST">
 6     {% csrf_token %}
 7 
 8     <div class="table-responsive">
 9         <table class="table table-striped">
10             <thead>
11             <tr>
12                 <th>id</th>
13                 <th>测试集合名称</th>
14                 <th>建立者</th>
15                 <th>建立时间</th>
16                 <th>查看/删除测试用例</th>
17                 <th>添加测试用例</th>
18                 <th>用例集合执行结果</th>
19             </tr>
20             </thead>
21             <tbody>
22 
23             {% for case_suite in case_suites %}
24             <tr>
25                 <td>{{ case_suite.id }}</td>
26                 <td>{{ case_suite.suite_desc }}</td>
27                 <td>{{ case_suite.creator }}</td>
28                 <td>{{ case_suite.create_time|date:"Y-n-d H:i" }}</td>
29                 <td><a href="{% url 'show_and_delete_case_in_suite' case_suite.id %}">查看/删除测试用例</a></td>
30                 <td><a href="{% url 'add_case_in_suite' case_suite.id %}">添加测试用例</a></td>
31                 <td><a href="">查看用例集合执行结果</a></td>
32             </tr>
33             {% endfor %}
34             </tbody>
35         </table>
36     </div>
37 </form>
38 
39 {# 实现分页标签的代码 #}
40 {# 这里使用 bootstrap 渲染页面 #}
41 <div id="pages" class="text-center">
42     <nav>
43         <ul class="pagination">
44             <li class="step-links">
45                 {% if case_suites.has_previous %}
46                 <a class='active' href="?page={{ case_suites.previous_page_number }}">上一页</a>
47                 {% endif %}
48 
49                 <span class="current">
50                     第 {{ case_suites.number }} 页 / 共 {{ case_suites.paginator.num_pages }} 页</span>
51 
52                 {% if case_suites.has_next %}
53                 <a class='active' href="?page={{ case_suites.next_page_number }}">下一页</a>
54                 {% endif %}
55             </li>
56         </ul>
57     </nav>
58 </div>
59 {% endblock %}

 

8. 测试用例执行 

预期效果以下:

用例执行逻辑以下:

  1. 前端提交用例 id 列表到后台,后台获取每一条用例的信息;
  2. 后台获取域名信息、用例 id 列表;
  3. 对用例的请求数据进行变量的参数化、函数化等预处理操做;
  4. 根据前后顺序进行接口请求,并对响应数据进行断言;
  5. 根据用例中的提取变量表达式,从断言成功的响应数据中提取关联变量值用于后续用例使用。

8.1 修改测试用例页模板文件:前端提交用例信息 

templates/test_case.html:

  1 {% extends 'base.html' %}
  2 {% load static %}
  3 {% block title %}测试用例{% endblock %}
  4 
  5 {% block content %}
  6 <script type="text/javascript">
  7         //页面加载的时候,全部的复选框都是未选中的状态
  8         function checkOrCancelAll() {
  9             var all_check = document.getElementById("all_check");  //1.获取all的元素对象
 10             var all_check = all_check.checked;  //2.获取选中状态
 11             //3.若checked=true,将全部的复选框选中;checked=false,将全部的复选框取消
 12             var allCheck = document.getElementsByName("test_cases_list");
 13             //4.循环遍历取出每个复选框中的元素
 14             if (all_check)//全选
 15             {
 16                 for (var i = 0; i < allCheck.length; i++) {
 17                     //设置复选框的选中状态
 18                     allCheck[i].checked = true;
 19                 }
 20             } else//取消全选
 21             {
 22                 for (var i = 0; i < allCheck.length; i++) {
 23                     allCheck[i].checked = false;
 24                 }
 25             }
 26         }
 27 
 28         function ischecked() {
 29             //3.若checked=true,将全部的复选框选中,checked=false,将全部的复选框取消
 30             var allCheck = document.getElementsByName("test_cases_list");
 31             for (var i = 0; i < allCheck.length; i++) {
 32                 if (allCheck[i].checked == true) {
 33                     alert("所需执行的测试用例提交成功!");
 34                     return true
 35                 }
 36             }
 37             alert("请选择要执行的测试用例!")
 38             return false
 39         }
 40 
 41 </script>
 42 
 43 <form action="" method="POST">
 44     {% csrf_token %}
 45     <input style="margin-left: 5px;" type="submit" value='执行测试用例' onclick="return ischecked()"/>
 46     <span style="margin-left: 5px;">运行环境:</span>
 47     <select name="env">
 48         <option selected value="dev">dev</option>
 49         <option value="prod">prod</option>
 50     </select>
 51     <div class="table-responsive">
 52         <table class="table table-striped">
 53             <thead>
 54             <tr>
 55                 <th><input type="checkbox" id="all_check" onclick="checkOrCancelAll();"/>全选</th>
 56                 <th>用例名称</th>
 57                 <th>所属项目</th>
 58                 <th>所属模块</th>
 59                 <th>接口地址</th>
 60                 <th>请求方式</th>
 61                 <th>请求数据</th>
 62                 <th>断言key</th>
 63                 <th>提取变量表达式</th>
 64             </tr>
 65             </thead>
 66             <tbody>
 67 
 68             {% for test_case in test_cases %}
 69             <tr>
 70                 <td><input type="checkbox" value="{{ test_case.id }}" name="test_cases_list"> {{ test_case.id }}</td>
 71                 <td><a href="{% url 'test_case_detail' test_case.id%}">{{ test_case.case_name }}</a></td>
 72                 <td>{{ test_case.belong_project.name }}</td>
 73                 <td>{{ test_case.belong_module.name }}</td>
 74                 <td>{{ test_case.uri }}</td>
 75                 <td>{{ test_case.request_method }}</td>
 76                 <td>{{ test_case.request_data }}</td>
 77                 <td>{{ test_case.assert_key }}</td>
 78                 <td>{{ test_case.extract_var }}</td>
 79             </tr>
 80             {% endfor %}
 81             </tbody>
 82         </table>
 83 
 84     </div>
 85 </form>
 86 {# 实现分页标签的代码 #}
 87 {# 这里使用 bootstrap 渲染页面 #}
 88 <div id="pages" class="text-center">
 89     <nav>
 90         <ul class="pagination">
 91             <li class="step-links">
 92                 {% if test_cases.has_previous %}
 93                 <a class='active' href="?page={{ test_cases.previous_page_number }}">上一页</a>
 94                 {% endif %}
 95 
 96                 <span class="current">
 97                     第 {{ test_cases.number }} 页 / 共 {{ test_cases.paginator.num_pages }} 页</span>
 98 
 99                 {% if test_cases.has_next %}
100                 <a class='active' href="?page={{ test_cases.next_page_number }}">下一页</a>
101                 {% endif %}
102             </li>
103         </ul>
104     </nav>
105 </div>
106 {% endblock %}

8.2 定义接口地址模型类

models.py:

  1 from django.db import models
  2 from smart_selects.db_fields import GroupedForeignKey  # pip install django-smart-selects:后台级联选择
  3 from django.contrib.auth.models import User
  4 
  5 
  6 class Project(models.Model):
  7     id = models.AutoField(primary_key=True)
  8     name = models.CharField('项目名称', max_length=50, unique=True, null=False)
  9     proj_owner = models.CharField('项目负责人', max_length=20, null=False)
 10     test_owner = models.CharField('测试负责人', max_length=20, null=False)
 11     dev_owner = models.CharField('开发负责人', max_length=20, null=False)
 12     desc = models.CharField('项目描述', max_length=100, null=True)
 13     create_time = models.DateTimeField('项目建立时间', auto_now_add=True)
 14     update_time = models.DateTimeField('项目更新时间', auto_now=True, null=True)
 15 
 16     def __str__(self):
 17         return self.name
 18 
 19     class Meta:
 20         verbose_name = '项目信息表'
 21         verbose_name_plural = '项目信息表'
 22 
 23 
 24 class Module(models.Model):
 25     id = models.AutoField(primary_key=True)
 26     name = models.CharField('模块名称', max_length=50, null=False)
 27     belong_project = models.ForeignKey(Project, on_delete=models.CASCADE)
 28     test_owner = models.CharField('测试负责人', max_length=50, null=False)
 29     desc = models.CharField('简要描述', max_length=100, null=True)
 30     create_time = models.DateTimeField('建立时间', auto_now_add=True)
 31     update_time = models.DateTimeField('更新时间', auto_now=True, null=True)
 32 
 33     def __str__(self):
 34         return self.name
 35 
 36     class Meta:
 37         verbose_name = '模块信息表'
 38         verbose_name_plural = '模块信息表'
 39 
 40 
 41 class TestCase(models.Model):
 42     id = models.AutoField(primary_key=True)
 43     case_name = models.CharField('用例名称', max_length=50, null=False)  # 如 register
 44     belong_project = models.ForeignKey(Project, on_delete=models.CASCADE, verbose_name='所属项目')
 45     belong_module = GroupedForeignKey(Module, "belong_project", on_delete=models.CASCADE, verbose_name='所属模块')
 46     request_data = models.CharField('请求数据', max_length=1024, null=False, default='')
 47     uri = models.CharField('接口地址', max_length=1024, null=False, default='')
 48     assert_key = models.CharField('断言内容', max_length=1024, null=True)
 49     maintainer = models.CharField('编写人员', max_length=1024, null=False, default='')
 50     extract_var = models.CharField('提取变量表达式', max_length=1024, null=True)  # 示例:userid||userid": (\d+)
 51     request_method = models.CharField('请求方式', max_length=1024, null=True)
 52     status = models.IntegerField(null=True, help_text="0:表示有效,1:表示无效,用于软删除")
 53     created_time = models.DateTimeField('建立时间', auto_now_add=True)
 54     updated_time = models.DateTimeField('更新时间', auto_now=True, null=True)
 55     user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='责任人', null=True)
 56 
 57     def __str__(self):
 58         return self.case_name
 59 
 60     class Meta:
 61         verbose_name = '测试用例表'
 62         verbose_name_plural = '测试用例表'
 63 
 64 
 65 class CaseSuite(models.Model):
 66     id = models.AutoField(primary_key=True)
 67     suite_desc = models.CharField('用例集合描述', max_length=100, blank=True, null=True)
 68     if_execute = models.IntegerField(verbose_name='是否执行', null=False, default=0, help_text='0:执行;1:不执行')
 69     test_case_model = models.CharField('测试执行模式', max_length=100, blank=True, null=True, help_text='data/keyword')
 70     creator = models.CharField(max_length=50, blank=True, null=True)
 71     create_time = models.DateTimeField('建立时间', auto_now=True)  # 建立时间-自动获取当前时间
 72 
 73     class Meta:
 74         verbose_name = "用例集合表"
 75         verbose_name_plural = '用例集合表'
 76 
 77 
 78 class SuiteCase(models.Model):
 79     id = models.AutoField(primary_key=True)
 80     case_suite = models.ForeignKey(CaseSuite, on_delete=models.CASCADE, verbose_name='用例集合')
 81     test_case = models.ForeignKey(TestCase, on_delete=models.CASCADE, verbose_name='测试用例')
 82     status = models.IntegerField(verbose_name='是否有效', null=False, default=1, help_text='0:有效,1:无效')
 83     create_time = models.DateTimeField('建立时间', auto_now=True)  # 建立时间-自动获取当前时间
 84 
 85 
 86 class InterfaceServer(models.Model):
 87     id = models.AutoField(primary_key=True)
 88     env = models.CharField('环境', max_length=50, null=False, default='')
 89     ip = models.CharField('ip', max_length=50, null=False, default='')
 90     port = models.CharField('端口', max_length=100, null=False, default='')
 91     remark = models.CharField('备注', max_length=100, null=True)
 92     create_time = models.DateTimeField('建立时间', auto_now_add=True)
 93     update_time = models.DateTimeField('更新时间', auto_now=True, null=True)
 94 
 95     def __str__(self):
 96         return self.env
 97 
 98     class Meta:
 99         verbose_name = '接口地址配置表'
100         verbose_name_plural = '接口地址配置表'

执行数据迁移:

python manage.py makemigrations
python manage.py migrate

admin.py:

 1 from django.contrib import admin
 2 from .import models
 3 
 4 
 5 class ProjectAdmin(admin.ModelAdmin):
 6     list_display = ("id", "name", "proj_owner", "test_owner", "dev_owner", "desc", "create_time", "update_time")
 7 
 8 admin.site.register(models.Project, ProjectAdmin)
 9 
10 
11 class ModuleAdmin(admin.ModelAdmin):
12     list_display = ("id", "name", "belong_project", "test_owner", "desc", "create_time", "update_time")
13 
14 admin.site.register(models.Module, ModuleAdmin)
15 
16 
17 class TestCaseAdmin(admin.ModelAdmin):
18     list_display = (
19         "id", "case_name", "belong_project", "belong_module", "request_data", "uri", "assert_key", "maintainer",
20         "extract_var", "request_method", "status", "created_time", "updated_time", "user")
21 
22 admin.site.register(models.TestCase, TestCaseAdmin)
23 
24 
25 class CaseSuiteAdmin(admin.ModelAdmin):
26     list_display = ("id", "suite_desc", "creator", "create_time")
27 
28 admin.site.register(models.CaseSuite, CaseSuiteAdmin)
29 
30 
31 class InterfaceServerAdmin(admin.ModelAdmin):
32     list_display = ("id", "env", "ip", "port", "remark", "create_time")
33 
34 admin.site.register(models.InterfaceServer, InterfaceServerAdmin)

登陆 admin 系统,添加地址配置数据:

8.3 修改测试用例视图函数,后台执行用例

1)Redis 持久化递增惟一数

在本工程中,咱们使用 Redis 来维护一个每次调用函数来就会递增的数值,供注册接口的注册用户名拼接使用,避免注册接口请求数据重复使用问题。

1.1)Redis 持久化配置

修改 redis.windows.conf:

appendonly yes  # 每次更新操做后进行日志记录
appendfsync everysec  # 每秒同步一次(默认值)

1.2)启动 Redis 服务端:

redis-server.exe redis.windows.conf

2)请求/响应数据处理

在应用目录下新建 utils 包,用于封装接口请求的相关函数。

data_process.py

该模块实现了对接口请求的所需工具函数,如获取递增惟一数(供注册用户名使用)、md5 加密(用于登陆密码加密)、请求数据预处理、响应数据断言等功能。

  • get_unique_num_value():用于获取每次递增的惟一数
    • 该函数的目标是解决注册用户名重复的问题。
    • 虽然能够在赋值注册用户名变量时,采用前缀字符串拼接随机数的方式,可是用随机数的方式仍然是有可能出现用户名重复的状况。所以,能够在单独的一个文件中维护一个数字,每次请求注册接口以前,先读取该文件中的数字,拼接用户名前缀字符串。读取完以后,再把这个数字进行加一的操做并保存,即每读取一次这个数字以后,就作一次修改,进而保证每次拼接的用户名都是惟一的,避免出现由于用户名重复致使用例执行失败的状况。
  • data_preprocess():对请求数据进行预处理:参数化及函数化。
  • data_postprocess():将响应数据须要关联的参数保存进全局变量,供后续接口使用。
  • assert_result():对响应数据进行关键字断言。
  1 import re
  2 import hashlib
  3 import os
  4 import json
  5 import traceback
  6 import redis
  7 from InterfaceAutoTest.settings import redis_port
  8 
  9 
 10 # 链接redis
 11 pool = redis.ConnectionPool(host='localhost', port=redis_port, decode_responses=True)
 12 redis_obj = redis.Redis(connection_pool=pool)
 13 
 14 
 15 # 初始化框架工程中的全局变量,存储在测试数据中的惟一值数据
 16 # 框架工程中若要使用字典中的任意一个变量,则每次使用后,均须要将字典中的value值进行加1操做。
 17 def get_unique_number_value(unique_number):
 18     data = None
 19     try:
 20         redis_value = redis_obj.get(unique_number)  # {"unique_number": 666}
 21         if redis_value:
 22             data = redis_value
 23             print("全局惟一数当前生成的值是:%s" % data)
 24             # 把redis中key为unique_number的值进行加一操做,以便下提取时保持惟一
 25             redis_obj.set(unique_number, int(redis_value) + 1)
 26         else:
 27             data = 1000  # 初始化递增数值
 28             redis_obj.set(unique_number, data)
 29     except Exception as e:
 30         print("获取全局惟一数变量值失败,请求的全局惟一数变量是%s,异常缘由以下:%s" % (unique_number, traceback.format_exc()))
 31         data = None
 32     finally:
 33         return data
 34 
 35 
 36 def md5(s):
 37     m5 = hashlib.md5()
 38     m5.update(s.encode("utf-8"))
 39     md5_value = m5.hexdigest()
 40     return md5_value
 41 
 42 
 43 # 请求数据预处理:参数化、函数化
 44 # 将请求数据中包含的${变量名}的字符串部分,替换为惟一数或者全局变量字典中对应的全局变量
 45 def data_preprocess(global_key, requestData):
 46     try:
 47         # 匹配注册用户名参数,即"${unique_num...}"的格式,并取出本次请求的随机数供后续接口的用户名参数使用
 48         if re.search(r"\$\{unique_num\d+\}", requestData):
 49             var_name = re.search(r"\$\{(unique_num\d+)\}", requestData).group(1)  # 获取用户名参数
 50             print("用户名变量:%s" % var_name)
 51             var_value = get_unique_number_value(var_name)
 52             print("用户名变量值: %s" % var_value)
 53             requestData = re.sub(r"\$\{unique_num\d+\}", str(var_value), requestData)
 54             var_name = var_name.split("_")[1]
 55             print("关联的用户名变量: %s" % var_name)
 56             # "xxxkey" : "{'var_name': var_value}"
 57             global_var = json.loads(os.environ[global_key])
 58             global_var[var_name] = var_value
 59             os.environ[global_key] = json.dumps(global_var)
 60             print("用户名惟一数参数化后的全局变量【os.environ[global_key]】: {}".format(os.environ[global_key]))
 61         # 函数化,如密码加密"${md5(...)}"的格式
 62         if re.search(r"\$\{\w+\(.+\)\}", requestData):
 63             var_pass = re.search(r"\$\{(\w+\(.+\))\}", requestData).group(1)  # 获取密码参数
 64             print("须要函数化的变量: %s" % var_pass)
 65             print("函数化后的结果: %s" % eval(var_pass))
 66             requestData = re.sub(r"\$\{\w+\(.+\)\}", eval(var_pass), requestData)  # 将requestBody里面的参数内容经过eval修改成实际变量值
 67             print("函数化后的请求数据: %s" % requestData)  # requestBody是拿到的请求时发送的数据
 68         # 其他变量参数化
 69         if re.search(r"\$\{(\w+)\}", requestData):
 70             print("须要参数化的变量: %s" % (re.findall(r"\$\{(\w+)\}", requestData)))
 71             for var_name in re.findall(r"\$\{(\w+)\}", requestData):
 72                 requestData = re.sub(r"\$\{%s\}" % var_name, str(json.loads(os.environ[global_key])[var_name]), requestData)
 73         print("变量参数化后的最终请求数据: %s" % requestData)
 74         print("数据参数后的最终全局变量【os.environ[global_key]】: {}".format(os.environ[global_key]))
 75         return 0, requestData, ""
 76     except Exception as e:
 77         print("请求数据预处理发生异常,error:{}".format(traceback.format_exc()))
 78         return 1, {}, traceback.format_exc()
 79 
 80 
 81 # 响应数据提取关联参数
 82 def data_postprocess(global_key, response_data, extract_var):
 83     print("需提取的关联变量:%s" % extract_var)
 84     var_name = extract_var.split("||")[0]
 85     print("关联变量名:%s" % var_name)
 86     regx_exp = extract_var.split("||")[1]
 87     print("关联变量正则:%s" % regx_exp)
 88     if re.search(regx_exp, response_data):
 89         global_vars = json.loads(os.environ[global_key])
 90         print("关联前的全局变量:{}".format(global_vars))
 91         global_vars[var_name] = re.search(regx_exp, response_data).group(1)
 92         os.environ[global_key] = json.dumps(global_vars)
 93         print("关联前的全局变量:{}".format(os.environ[global_key]))
 94     return
 95 
 96 
 97 # 响应数据 断言处理
 98 def assert_result(response_obj, key_word):
 99     try:
100         # 多个断言关键字
101         if '&&' in key_word:
102             key_word_list = key_word.split('&&')
103             print("断言关键字列表:%s" % key_word_list)
104             # 断言结果标识符
105             flag = True
106             exception_info = ''
107             # 遍历分隔出来的断言关键词列表
108             for key_word in key_word_list:
109                 # 若是断言词非空,则进行断言
110                 if key_word:
111                     # 没查到断言词则认为是断言失败
112                     if not (key_word in json.dumps(response_obj.json(), ensure_ascii=False)):
113                         print("断言关键字【{}】匹配失败".format(key_word))
114                         flag = False  # 只要有一个断言词匹配失败,则整个接口断言失败
115                         exception_info = "keyword: {} not matched from response, assert failed".format(key_word)
116                     else:
117                         print("断言关键字【{}】匹配成功".format(key_word))
118             if flag:
119                 print("接口断言成功!")
120             else:
121                 print("接口断言失败!")
122             return flag, exception_info
123         # 单个断言关键字
124         else:
125             if key_word in json.dumps(response_obj.json(), ensure_ascii=False):
126                 print("接口断言【{}】匹配成功!".format(key_word))
127                 return True, ''
128             else:
129                 print("接口断言【{}】匹配失败!".format(key_word))
130                 return False, ''
131     except Exception as e:
132         return False, traceback.format_exc()
133 
134 
135 # 测试代码
136 if __name__ == "__main__":
137     print(get_unique_number_value("unique_num1"))

request_process.py

该模块实现了对接口请求的封装。

 1 import requests
 2 import json
 3 # from Util.Log import logger
 4 
 5 
 6 # 此函数封装了get请求、post和put请求的方法
 7 def request_process(url, request_method, request_content):
 8     print("-------- 开始调用接口 --------")
 9     if request_method == "get":
10         try:
11             if isinstance(request_content, dict):
12                 print("接口地址:%s" % url)
13                 print("请求数据:%s" % request_content)
14                 r = requests.get(url, params=json.dumps(request_content))
15             else:
16                 r = requests.get(url+str(request_content))
17                 print("接口地址:%s" % r.url)
18                 print("请求数据:%s" % request_content)
19 
20         except Exception as e:
21             print("get方法请求发生异常:请求的url是%s, 请求的内容是%s\n发生的异常信息以下:%s" % (url, request_content, e))
22             r = None
23         return r
24     elif request_method == "post":
25         try:
26             if isinstance(request_content, dict):
27                 print("接口地址:%s" % url)
28                 print("请求数据:%s" % json.dumps(request_content))
29                 r = requests.post(url, data=json.dumps(request_content))
30             else:
31                 raise ValueError
32         except ValueError as e:
33             print("post方法请求发生异常:请求的url是%s, 请求的内容是%s\n发生的异常信息以下:%s" % (url, request_content, "请求参数不是字典类型"))
34             r = None
35         except Exception as e:
36             print("post方法请求发生异常:请求的url是%s, 请求的内容是%s\n发生的异常信息以下:%s" % (url, request_content, e))
37             r = None
38         return r
39     elif request_method == "put":
40         try:
41             if isinstance(request_content, dict):
42                 print("接口地址:%s" % url)
43                 print("请求数据:%s" % json.dumps(request_content))
44                 r = requests.put(url,  data=json.dumps(request_content))
45             else:
46                 raise ValueError
47         except ValueError as e:
48             print("put方法请求发生异常:请求的url是%s, 请求的内容是%s\n发生的异常信息以下:%s" % (url,  request_content, "请求参数不是字典类型"))
49             r = None
50         except Exception as e:
51             print("put方法请求发生异常:请求的url是%s, 请求的内容是%s\n发生的异常信息以下:%s" % (url, request_content, e))
52             r = None
53         return r

3)封装接口用例执行方法

在应用目录下新建 task.py: 

 1 import time
 2 import os
 3 import traceback
 4 import json
 5 from . import models
 6 from .utils.data_process import data_preprocess, assert_result, data_postprocess
 7 from .utils.request_process import request_process
 8 
 9 
10 def case_task(test_case_id_list, server_address):
11     global_key = 'case'+ str(int(time.time() * 100000))
12     os.environ[global_key] = '{}'
13     print()
14     print("全局变量标识符【global_key】: {}".format(global_key))
15     print("全局变量内容【os.environ[global_key]】: {}".format(os.environ[global_key]))
16     for test_case_id in test_case_id_list:
17         print()
18         test_case = models.TestCase.objects.filter(id=int(test_case_id))[0]
19         print("######### 开始执行用例【{}】 #########".format(test_case))
20         execute_start_time = time.time()  # 记录时间戳,便于计算总耗时(毫秒)
21         request_data = test_case.request_data
22         extract_var = test_case.extract_var
23         assert_key = test_case.assert_key
24         interface_name = test_case.uri
25         belong_project = test_case.belong_project
26         belong_module = test_case.belong_module
27         maintainer = test_case.maintainer
28         request_method = test_case.request_method
29         print("初始请求数据: {}".format(request_data))
30         print("关联参数: {}".format(extract_var))
31         print("断言关键字: {}".format(assert_key))
32         print("接口名称: {}".format(interface_name))
33         print("所属项目: {}".format(belong_project))
34         print("所属模块: {}".format(belong_module))
35         print("用例维护人: {}".format(maintainer))
36         print("请求方法: {}".format(request_method))
37         url = "{}{}".format(server_address, interface_name)
38         print("接口地址: {}".format(url))
39         code, request_data, error_msg = data_preprocess(global_key, str(request_data))
40         try:
41             res_data = request_process(url, request_method, json.loads(request_data))
42             print("响应数据: {}".format(json.dumps(res_data.json(), ensure_ascii=False)))  # ensure_ascii:兼容中文
43             result_flag, exception_info = assert_result(res_data, assert_key)
44             if result_flag:
45                 print("用例【%s】执行成功!" % test_case)
46                 if extract_var.strip() != "None":
47                     data_postprocess(global_key, json.dumps(res_data.json(), ensure_ascii=False), extract_var)
48             else:
49                 print("用例【%s】执行失败!" % test_case)
50         except Exception as e:
51             print("接口请求异常,error: {}".format(traceback.format_exc()))

4)修改测试用例视图函数

  1 from django.shortcuts import render, redirect, HttpResponse
  2 from django.contrib import auth  # Django用户认证(Auth)组件通常用在用户的登陆注册上,用于判断当前的用户是否合法
  3 from django.contrib.auth.decorators import login_required
  4 from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage
  5 from .form import UserForm
  6 import traceback
  7 from .models import Project, Module, TestCase, CaseSuite, SuiteCase, InterfaceServer
  8 from .task import case_task
  9 
 10 
 11 # 封装分页处理
 12 def get_paginator(request, data):
 13     paginator = Paginator(data, 10)  # 默认每页展现10条数据
 14     # 获取 url 后面的 page 参数的值, 首页不显示 page 参数, 默认值是 1
 15     page = request.GET.get('page')
 16     try:
 17         paginator_pages = paginator.page(page)
 18     except PageNotAnInteger:
 19         # 若是请求的页数不是整数, 返回第一页。
 20         paginator_pages = paginator.page(1)
 21     except InvalidPage:
 22         # 若是请求的页数不存在, 重定向页面
 23         return HttpResponse('找不到页面的内容')
 24     return paginator_pages
 25 
 26 
 27 # 项目菜单项
 28 @login_required
 29 def project(request):
 30     print("request.user.is_authenticated: ", request.user.is_authenticated)
 31     projects = Project.objects.filter().order_by('-id')
 32     print("projects:", projects)
 33     return render(request, 'project.html', {'projects': get_paginator(request, projects)})
 34 
 35 
 36 # 模块菜单项
 37 @login_required
 38 def module(request):
 39     if request.method == "GET":  # 请求get时候,id倒序查询全部的模块数据
 40         modules = Module.objects.filter().order_by('-id')
 41         return render(request, 'module.html', {'modules': get_paginator(request, modules)})
 42     else:  # 不然就是Post请求,会根据输入内容,使用模糊的方式查找全部的项目
 43         proj_name = request.POST['proj_name']
 44         projects = Project.objects.filter(name__contains=proj_name.strip())
 45         projs = [proj.id for proj in projects]
 46         modules = Module.objects.filter(belong_project__in=projs)  # 把项目中全部的模块都找出来
 47         return render(request, 'module.html', {'modules': get_paginator(request, modules), 'proj_name': proj_name})
 48 
 49 
 50 # 获取测试用例执行的接口地址
 51 def get_server_address(env):
 52     if env:  # 环境处理
 53         env_data = InterfaceServer.objects.filter(env=env[0])
 54         print("env_data: {}".format(env_data))
 55         if env_data:
 56             ip = env_data[0].ip
 57             port = env_data[0].port
 58             print("ip: {}, port: {}".format(ip, port))
 59             server_address = "http://{}:{}".format(ip, port)
 60             print("server_address: {}".format(server_address))
 61             return server_address
 62         else:
 63             return ""
 64     else:
 65         return ""
 66 
 67 
 68 # 测试用例菜单项
 69 @login_required
 70 def test_case(request):
 71     print("request.session['is_login']: {}".format(request.session['is_login']))
 72     test_cases = ""
 73     if request.method == "GET":
 74         test_cases = TestCase.objects.filter().order_by('id')
 75         print("testcases: {}".format(test_cases))
 76     elif request.method == "POST":
 77         print("request.POST: {}".format(request.POST))
 78         test_case_id_list = request.POST.getlist('test_cases_list')
 79         env = request.POST.getlist('env')
 80         print("env: {}".format(env))
 81         server_address = get_server_address(env)
 82         if not server_address:
 83             return HttpResponse("提交的运行环境为空,请选择环境后再提交!")
 84         if test_case_id_list:
 85             test_case_id_list.sort()
 86             print("test_case_id_list: {}".format(test_case_id_list))
 87             print("获取到用例,开始用例执行")
 88             case_task(test_case_id_list, server_address)
 89         else:
 90             print("运行测试用例失败")
 91             return HttpResponse("提交的运行测试用例为空,请选择用例后在提交!")
 92         test_cases = TestCase.objects.filter().order_by('id')
 93     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
 94 
 95 
 96 # 用例详情页
 97 @login_required
 98 def test_case_detail(request, test_case_id):
 99     test_case_id = int(test_case_id)
100     test_case = TestCase.objects.get(id=test_case_id)
101     print("test_case: {}".format(test_case))
102     print("test_case.id: {}".format(test_case.id))
103     print("test_case.belong_project: {}".format(test_case.belong_project))
104 
105     return render(request, 'test_case_detail.html', {'test_case': test_case})
106 
107 
108 # 模块页展现测试用例
109 @login_required
110 def module_test_cases(request, module_id):
111     module = ""
112     if module_id:  # 访问的时候,会从url中提取模块的id,根据模块id查询到模块数据,在模板中展示
113         module = Module.objects.get(id=int(module_id))
114     test_cases = TestCase.objects.filter(belong_module=module).order_by('-id')
115     print("test_case in module_test_cases: {}".format(test_cases))
116     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
117 
118 
119 # 用例集合菜单项
120 @login_required
121 def case_suite(request):
122     case_suites = CaseSuite.objects.filter()
123     return render(request, 'case_suite.html', {'case_suites': get_paginator(request, case_suites)})
124 
125 
126 # 用例集合-添加测试用例页
127 @login_required
128 def add_case_in_suite(request, suite_id):
129     # 查询指定的用例集合
130     case_suite = CaseSuite.objects.get(id=suite_id)
131     # 根据id号查询全部的用例
132     test_cases = TestCase.objects.filter().order_by('id')
133     if request.method == "GET":
134         print("test cases:", test_cases)
135     elif request.method == "POST":
136         test_cases_list = request.POST.getlist('testcases_list')
137         # 若是页面勾选了用例
138         if test_cases_list:
139             print("勾选用例id:", test_cases_list)
140             # 根据页面勾选的用例与查询出的全部用例一一比较
141             for test_case in test_cases_list:
142                 test_case = TestCase.objects.get(id=int(test_case))
143                 # 匹配成功则添加用例
144                 SuiteCase.objects.create(case_suite=case_suite, test_case=test_case)
145         # 未勾选用例
146         else:
147             print("添加测试用例失败")
148             return HttpResponse("添加的测试用例为空,请选择用例后再添加!")
149     return render(request, 'add_case_in_suite.html',
150           {'test_cases': get_paginator(request, test_cases), 'case_suite': case_suite})
151 
152 
153 # 用例集合页-查看/删除用例
154 @login_required
155 def show_and_delete_case_in_suite(request, suite_id):
156     case_suite = CaseSuite.objects.get(id=suite_id)
157     test_cases = SuiteCase.objects.filter(case_suite=case_suite)
158     if request.method == "POST":
159         test_cases_list = request.POST.getlist('test_cases_list')
160         if test_cases_list:
161             print("勾选用例:", test_cases_list)
162             for test_case in test_cases_list:
163                 test_case = TestCase.objects.get(id=int(test_case))
164                 SuiteCase.objects.filter(case_suite=case_suite, test_case=test_case).first().delete()
165         else:
166             print("测试用例删除失败")
167             return HttpResponse("所选测试用例为空,请选择用例后再进行删除!")
168     case_suite = CaseSuite.objects.get(id=suite_id)
169     return render(request, 'show_and_delete_case_in_suite.html',
170                   {'test_cases': get_paginator(request, test_cases), 'case_suite': case_suite})
171 
172 
173 # 默认页的视图函数
174 @login_required
175 def index(request):
176     return render(request, 'index.html')
177 
178 
179 # 登陆页的视图函数
180 def login(request):
181     print("request.session.items(): {}".format(request.session.items()))  # 打印session信息
182     if request.session.get('is_login', None):
183         return redirect('/')
184     # 若是是表单提交行为,则进行登陆校验
185     if request.method == "POST":
186         login_form = UserForm(request.POST)
187         message = "请检查填写的内容!"
188         if login_form.is_valid():
189             username = login_form.cleaned_data['username']
190             password = login_form.cleaned_data['password']
191             try:
192                 # 使用django提供的身份验证功能
193                 user = auth.authenticate(username=username, password=password)  # 从auth_user表中匹配信息,匹配到则返回用户对象
194                 if user is not None:
195                     print("用户【%s】登陆成功" % username)
196                     auth.login(request, user)
197                     request.session['is_login'] = True
198                     # 登陆成功,跳转主页
199                     return redirect('/')
200                 else:
201                     message = "用户名不存在或者密码不正确!"
202             except:
203                 traceback.print_exc()
204                 message = "登陆程序出现异常"
205         # 用户名或密码为空,返回登陆页和错误提示信息
206         else:
207             return render(request, 'login.html', locals())
208     # 不是表单提交,表明只是访问登陆页
209     else:
210         login_form = UserForm()
211         return render(request, 'login.html', locals())
212 
213 
214 # 注册页的视图函数
215 def register(request):
216     return render(request, 'register.html')
217 
218 
219 # 登出的视图函数:重定向至login视图函数
220 @login_required
221 def logout(request):
222     auth.logout(request)
223     request.session.flush()
224     return redirect("/login/")

 

9. 用例执行结果展现

9.1 定义模型类

1)models.py 中增长 TestCaseExecuteResult 模型类,用于记录用例执行结果。

  1 from django.db import models
  2 from smart_selects.db_fields import GroupedForeignKey  # pip install django-smart-selects:后台级联选择
  3 from django.contrib.auth.models import User
  4 
  5 
  6 class Project(models.Model):
  7     id = models.AutoField(primary_key=True)
  8     name = models.CharField('项目名称', max_length=50, unique=True, null=False)
  9     proj_owner = models.CharField('项目负责人', max_length=20, null=False)
 10     test_owner = models.CharField('测试负责人', max_length=20, null=False)
 11     dev_owner = models.CharField('开发负责人', max_length=20, null=False)
 12     desc = models.CharField('项目描述', max_length=100, null=True)
 13     create_time = models.DateTimeField('项目建立时间', auto_now_add=True)
 14     update_time = models.DateTimeField('项目更新时间', auto_now=True, null=True)
 15 
 16     def __str__(self):
 17         return self.name
 18 
 19     class Meta:
 20         verbose_name = '项目信息表'
 21         verbose_name_plural = '项目信息表'
 22 
 23 
 24 class Module(models.Model):
 25     id = models.AutoField(primary_key=True)
 26     name = models.CharField('模块名称', max_length=50, null=False)
 27     belong_project = models.ForeignKey(Project, on_delete=models.CASCADE)
 28     test_owner = models.CharField('测试负责人', max_length=50, null=False)
 29     desc = models.CharField('简要描述', max_length=100, null=True)
 30     create_time = models.DateTimeField('建立时间', auto_now_add=True)
 31     update_time = models.DateTimeField('更新时间', auto_now=True, null=True)
 32 
 33     def __str__(self):
 34         return self.name
 35 
 36     class Meta:
 37         verbose_name = '模块信息表'
 38         verbose_name_plural = '模块信息表'
 39 
 40 
 41 class TestCase(models.Model):
 42     id = models.AutoField(primary_key=True)
 43     case_name = models.CharField('用例名称', max_length=50, null=False)  # 如 register
 44     belong_project = models.ForeignKey(Project, on_delete=models.CASCADE, verbose_name='所属项目')
 45     belong_module = GroupedForeignKey(Module, "belong_project", on_delete=models.CASCADE, verbose_name='所属模块')
 46     request_data = models.CharField('请求数据', max_length=1024, null=False, default='')
 47     uri = models.CharField('接口地址', max_length=1024, null=False, default='')
 48     assert_key = models.CharField('断言内容', max_length=1024, null=True)
 49     maintainer = models.CharField('编写人员', max_length=1024, null=False, default='')
 50     extract_var = models.CharField('提取变量表达式', max_length=1024, null=True)  # 示例:userid||userid": (\d+)
 51     request_method = models.CharField('请求方式', max_length=1024, null=True)
 52     status = models.IntegerField(null=True, help_text="0:表示有效,1:表示无效,用于软删除")
 53     created_time = models.DateTimeField('建立时间', auto_now_add=True)
 54     updated_time = models.DateTimeField('更新时间', auto_now=True, null=True)
 55     user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='责任人', null=True)
 56 
 57     def __str__(self):
 58         return self.case_name
 59 
 60     class Meta:
 61         verbose_name = '测试用例表'
 62         verbose_name_plural = '测试用例表'
 63 
 64 
 65 class CaseSuite(models.Model):
 66     id = models.AutoField(primary_key=True)
 67     suite_desc = models.CharField('用例集合描述', max_length=100, blank=True, null=True)
 68     if_execute = models.IntegerField(verbose_name='是否执行', null=False, default=0, help_text='0:执行;1:不执行')
 69     test_case_model = models.CharField('测试执行模式', max_length=100, blank=True, null=True, help_text='data/keyword')
 70     creator = models.CharField(max_length=50, blank=True, null=True)
 71     create_time = models.DateTimeField('建立时间', auto_now=True)  # 建立时间-自动获取当前时间
 72 
 73     class Meta:
 74         verbose_name = "用例集合表"
 75         verbose_name_plural = '用例集合表'
 76 
 77 
 78 class SuiteCase(models.Model):
 79     id = models.AutoField(primary_key=True)
 80     case_suite = models.ForeignKey(CaseSuite, on_delete=models.CASCADE, verbose_name='用例集合')
 81     test_case = models.ForeignKey(TestCase, on_delete=models.CASCADE, verbose_name='测试用例')
 82     status = models.IntegerField(verbose_name='是否有效', null=False, default=1, help_text='0:有效,1:无效')
 83     create_time = models.DateTimeField('建立时间', auto_now=True)  # 建立时间-自动获取当前时间
 84 
 85 
 86 class InterfaceServer(models.Model):
 87     id = models.AutoField(primary_key=True)
 88     env = models.CharField('环境', max_length=50, null=False, default='')
 89     ip = models.CharField('ip', max_length=50, null=False, default='')
 90     port = models.CharField('端口', max_length=100, null=False, default='')
 91     remark = models.CharField('备注', max_length=100, null=True)
 92     create_time = models.DateTimeField('建立时间', auto_now_add=True)
 93     update_time = models.DateTimeField('更新时间', auto_now=True, null=True)
 94 
 95     def __str__(self):
 96         return self.env
 97 
 98     class Meta:
 99         verbose_name = '接口地址配置表'
100         verbose_name_plural = '接口地址配置表'
101 
102 
103 class TestCaseExecuteResult(models.Model):
104     id = models.AutoField(primary_key=True)
105     belong_test_case = GroupedForeignKey(TestCase, "belong_test_case", on_delete=models.CASCADE, verbose_name='所属用例')
106     status = models.IntegerField(null=True, help_text="0:表示未执行,1:表示已执行")
107     exception_info = models.CharField(max_length=2048, blank=True, null=True)
108     request_data = models.CharField('请求体', max_length=1024, null=True)  # {"code": "00", "userid": 22889}
109     response_data = models.CharField('响应字符串', max_length=1024, null=True)  # {"code": "00", "userid": 22889}
110     execute_result = models.CharField('执行结果', max_length=1024, null=True)  # 成功/失败
111     extract_var = models.CharField('关联参数', max_length=1024, null=True)  # 响应成功后提取变量
112     last_time_response_data = models.CharField('上一次响应字符串', max_length=1024, null=True)  # {"code": "00", "userid": 22889}
113     execute_total_time = models.CharField('执行耗时', max_length=1024, null=True)
114     execute_start_time = models.CharField('执行开始时间', max_length=300, blank=True, null=True)
115     execute_end_time = models.CharField('执行结束时间', max_length=300, blank=True, null=True)
116     created_time = models.DateTimeField('建立时间', auto_now_add=True)
117     updated_time = models.DateTimeField('更新时间', auto_now=True, null=True)
118 
119     def __str__(self):
120         return str(self.id)
121 
122     class Meta:
123         verbose_name = '用例执行结果记录表'
124         verbose_name_plural = '用例执行结果记录表'

2)数据迁移

python manage.py makemigrations
python manage.py migrate

9.2 修改用例执行封装函数,增长执行结果记录

修改应用目录下 task.py:

  1 import time
  2 import os
  3 import traceback
  4 import json
  5 from . import models
  6 from .utils.data_process import data_preprocess, assert_result, data_postprocess
  7 from .utils.request_process import request_process
  8 
  9 
 10 def case_task(test_case_id_list, server_address):
 11     global_key = 'case'+ str(int(time.time() * 100000))
 12     os.environ[global_key] = '{}'
 13     print()
 14     print("全局变量标识符【global_key】: {}".format(global_key))
 15     print("全局变量内容【os.environ[global_key]】: {}".format(os.environ[global_key]))
 16     for test_case_id in test_case_id_list:
 17 
 18         test_case = models.TestCase.objects.filter(id=int(test_case_id))[0]
 19         last_execute_record_data = models.TestCaseExecuteResult.objects.filter(
 20             belong_test_case_id=test_case_id).order_by('-id')
 21         if last_execute_record_data:
 22             last_time_execute_response_data = last_execute_record_data[0].response_data
 23         else:
 24             last_time_execute_response_data = ''
 25         print("上一次响应结果: {}".format(last_execute_record_data))
 26         print("上一次响应时间: {}".format(last_time_execute_response_data))
 27         execute_record = models.TestCaseExecuteResult.objects.create(belong_test_case=test_case)
 28         execute_record.last_time_response_data = last_time_execute_response_data
 29         # 获取当前用例上一次执行结果
 30         execute_record.save()
 31 
 32         test_case = models.TestCase.objects.filter(id=int(test_case_id))[0]
 33         print("\n######### 开始执行用例【{}】 #########".format(test_case))
 34         execute_start_time = time.time()  # 记录时间戳,便于计算总耗时(毫秒)
 35         execute_record.execute_start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(execute_start_time))
 36 
 37         request_data = test_case.request_data
 38         extract_var = test_case.extract_var
 39         assert_key = test_case.assert_key
 40         interface_name = test_case.uri
 41         belong_project = test_case.belong_project
 42         belong_module = test_case.belong_module
 43         maintainer = test_case.maintainer
 44         request_method = test_case.request_method
 45         print("初始请求数据: {}".format(request_data))
 46         print("关联参数: {}".format(extract_var))
 47         print("断言关键字: {}".format(assert_key))
 48         print("接口名称: {}".format(interface_name))
 49         print("所属项目: {}".format(belong_project))
 50         print("所属模块: {}".format(belong_module))
 51         print("用例维护人: {}".format(maintainer))
 52         print("请求方法: {}".format(request_method))
 53         url = "{}{}".format(server_address, interface_name)
 54         print("接口地址: {}".format(url))
 55         code, request_data, error_msg = data_preprocess(global_key, str(request_data))
 56         # 请求数据预处理异常,结束用例执行
 57         if code != 0:
 58             print("数据处理异常,error: {}".format(error_msg))
 59             execute_record.execute_result = "失败"
 60             execute_record.status = 1
 61             execute_record.exception_info = error_msg
 62             execute_end_time = time.time()
 63             execute_record.execute_end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(execute_end_time))
 64             execute_record.execute_total_time = int(execute_end_time - execute_start_time) * 1000
 65             execute_record.save()
 66             return
 67         # 记录请求预处理结果
 68         else:
 69             execute_record.request_data = request_data
 70         # 调用接口
 71         try:
 72             res_data = request_process(url, request_method, json.loads(request_data))
 73             print("响应数据: {}".format(json.dumps(res_data.json(), ensure_ascii=False)))  # ensure_ascii:兼容中文
 74             result_flag, exception_info = assert_result(res_data, assert_key)
 75             # 结果记录保存
 76             if result_flag:
 77                 print("用例【%s】执行成功!" % test_case)
 78                 execute_record.execute_result = "成功"
 79                 if extract_var.strip() != "None":
 80                     var_value = data_postprocess(global_key, json.dumps(res_data.json(), ensure_ascii=False), extract_var)
 81                     execute_record.extract_var = var_value
 82             else:
 83                 print("用例【%s】执行失败!" % test_case)
 84                 execute_record.execute_result = "失败"
 85                 execute_record.exception_info = exception_info
 86             execute_record.response_data = json.dumps(res_data.json(), ensure_ascii=False)
 87             execute_record.status = 1
 88             execute_end_time = time.time()
 89             execute_record.execute_end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(execute_end_time))
 90             print("执行结果结束时间: {}".format(execute_record.execute_end_time))
 91             execute_record.execute_total_time = int((execute_end_time - execute_start_time) * 1000)
 92             print("用例执行耗时: {}".format(execute_record.execute_total_time))
 93             execute_record.save()
 94         except Exception as e:
 95             print("接口请求异常,error: {}".format(traceback.format_exc()))
 96             execute_record.execute_result = "失败"
 97             execute_record.exception_info = traceback.format_exc()
 98             execute_record.status = 1
 99             execute_end_time = time.time()
100             execute_record.execute_end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(execute_end_time))
101             print("执行结果结束时间: {}".format(execute_record.execute_end_time))
102             execute_record.execute_total_time = int(execute_end_time - execute_start_time) * 1000
103             print("用例执行耗时: {} 毫秒".format(execute_record.execute_total_time))
104             execute_record.save()

前端执行测试用例,查看用例执行结果表数据:

9.3 定义路由 

在前面已经获取到用例结果数据并保存,下面处理一下用例结果展现。 

from django.urls import path, re_path
from . import views


urlpatterns = [
    path('', views.index),
    path('login/', views.login),
    path('logout/', views.logout),
    path('project/', views.project, name='project'),
    path('module/', views.module, name='module'),
    path('test_case/', views.test_case, name="test_case"),
    re_path('test_case_detail/(?P<test_case_id>[0-9]+)', views.test_case_detail, name="test_case_detail"),
    re_path('module_test_cases/(?P<module_id>[0-9]+)/$', views.module_test_cases, name="module_test_cases"),
    path('case_suite/', views.case_suite, name="case_suite"),
    re_path('add_case_in_suite/(?P<suite_id>[0-9]+)', views.add_case_in_suite, name="add_case_in_suite"),
    re_path('show_and_delete_case_in_suite/(?P<suite_id>[0-9]+)', views.show_and_delete_case_in_suite, name="show_and_delete_case_in_suite"),
    path('test_case_execute_record/', views.test_case_execute_record, name="test_case_execute_record"),
]

9.4 定义视图函数

  1 from django.shortcuts import render, redirect, HttpResponse
  2 from django.contrib import auth  # Django用户认证(Auth)组件通常用在用户的登陆注册上,用于判断当前的用户是否合法
  3 from django.contrib.auth.decorators import login_required
  4 from django.core.paginator import Paginator, PageNotAnInteger, InvalidPage
  5 from .form import UserForm
  6 import traceback
  7 from .models import Project, Module, TestCase, CaseSuite, SuiteCase, InterfaceServer, TestCaseExecuteResult
  8 from .task import case_task
  9 
 10 
 11 # 封装分页处理
 12 def get_paginator(request, data):
 13     paginator = Paginator(data, 10)  # 默认每页展现10条数据
 14     # 获取 url 后面的 page 参数的值, 首页不显示 page 参数, 默认值是 1
 15     page = request.GET.get('page')
 16     try:
 17         paginator_pages = paginator.page(page)
 18     except PageNotAnInteger:
 19         # 若是请求的页数不是整数, 返回第一页。
 20         paginator_pages = paginator.page(1)
 21     except InvalidPage:
 22         # 若是请求的页数不存在, 重定向页面
 23         return HttpResponse('找不到页面的内容')
 24     return paginator_pages
 25 
 26 
 27 # 项目菜单项
 28 @login_required
 29 def project(request):
 30     print("request.user.is_authenticated: ", request.user.is_authenticated)
 31     projects = Project.objects.filter().order_by('-id')
 32     print("projects:", projects)
 33     return render(request, 'project.html', {'projects': get_paginator(request, projects)})
 34 
 35 
 36 # 模块菜单项
 37 @login_required
 38 def module(request):
 39     if request.method == "GET":  # 请求get时候,id倒序查询全部的模块数据
 40         modules = Module.objects.filter().order_by('-id')
 41         return render(request, 'module.html', {'modules': get_paginator(request, modules)})
 42     else:  # 不然就是Post请求,会根据输入内容,使用模糊的方式查找全部的项目
 43         proj_name = request.POST['proj_name']
 44         projects = Project.objects.filter(name__contains=proj_name.strip())
 45         projs = [proj.id for proj in projects]
 46         modules = Module.objects.filter(belong_project__in=projs)  # 把项目中全部的模块都找出来
 47         return render(request, 'module.html', {'modules': get_paginator(request, modules), 'proj_name': proj_name})
 48 
 49 
 50 # 获取测试用例执行的接口地址
 51 def get_server_address(env):
 52     if env:  # 环境处理
 53         env_data = InterfaceServer.objects.filter(env=env[0])
 54         print("env_data: {}".format(env_data))
 55         if env_data:
 56             ip = env_data[0].ip
 57             port = env_data[0].port
 58             print("ip: {}, port: {}".format(ip, port))
 59             server_address = "http://{}:{}".format(ip, port)
 60             print("server_address: {}".format(server_address))
 61             return server_address
 62         else:
 63             return ""
 64     else:
 65         return ""
 66 
 67 
 68 # 测试用例菜单项
 69 @login_required
 70 def test_case(request):
 71     print("request.session['is_login']: {}".format(request.session['is_login']))
 72     test_cases = ""
 73     if request.method == "GET":
 74         test_cases = TestCase.objects.filter().order_by('id')
 75         print("testcases: {}".format(test_cases))
 76     elif request.method == "POST":
 77         print("request.POST: {}".format(request.POST))
 78         test_case_id_list = request.POST.getlist('test_cases_list')
 79         env = request.POST.getlist('env')
 80         print("env: {}".format(env))
 81         server_address = get_server_address(env)
 82         if not server_address:
 83             return HttpResponse("提交的运行环境为空,请选择环境后再提交!")
 84         if test_case_id_list:
 85             test_case_id_list.sort()
 86             print("test_case_id_list: {}".format(test_case_id_list))
 87             print("获取到用例,开始用例执行")
 88             case_task(test_case_id_list, server_address)
 89         else:
 90             print("运行测试用例失败")
 91             return HttpResponse("提交的运行测试用例为空,请选择用例后在提交!")
 92         test_cases = TestCase.objects.filter().order_by('id')
 93     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
 94 
 95 
 96 # 用例详情页
 97 @login_required
 98 def test_case_detail(request, test_case_id):
 99     test_case_id = int(test_case_id)
100     test_case = TestCase.objects.get(id=test_case_id)
101     print("test_case: {}".format(test_case))
102     print("test_case.id: {}".format(test_case.id))
103     print("test_case.belong_project: {}".format(test_case.belong_project))
104 
105     return render(request, 'test_case_detail.html', {'test_case': test_case})
106 
107 
108 # 模块页展现测试用例
109 @login_required
110 def module_test_cases(request, module_id):
111     module = ""
112     if module_id:  # 访问的时候,会从url中提取模块的id,根据模块id查询到模块数据,在模板中展示
113         module = Module.objects.get(id=int(module_id))
114     test_cases = TestCase.objects.filter(belong_module=module).order_by('-id')
115     print("test_case in module_test_cases: {}".format(test_cases))
116     return render(request, 'test_case.html', {'test_cases': get_paginator(request, test_cases)})
117 
118 
119 # 用例集合菜单项
120 @login_required
121 def case_suite(request):
122     case_suites = CaseSuite.objects.filter()
123     return render(request, 'case_suite.html', {'case_suites': get_paginator(request, case_suites)})
124 
125 
126 # 用例集合-添加测试用例页
127 @login_required
128 def add_case_in_suite(request, suite_id):
129     # 查询指定的用例集合
130     case_suite = CaseSuite.objects.get(id=suite_id)
131     # 根据id号查询全部的用例
132     test_cases = TestCase.objects.filter().order_by('id')
133     if request.method == "GET":
134         print("test cases:", test_cases)
135     elif request.method == "POST":
136         test_cases_list = request.POST.getlist('testcases_list')
137         # 若是页面勾选了用例
138         if test_cases_list:
139             print("勾选用例id:", test_cases_list)
140             # 根据页面勾选的用例与查询出的全部用例一一比较
141             for test_case in test_cases_list:
142                 test_case = TestCase.objects.get(id=int(test_case))
143                 # 匹配成功则添加用例
144                 SuiteCase.objects.create(case_suite=case_suite, test_case=test_case)
145         # 未勾选用例
146         else:
147             print("添加测试用例失败")
148             return HttpResponse("添加的测试用例为空,请选择用例后再添加!")
149     return render(request, 'add_case_in_suite.html',
150           {'test_cases': get_paginator(request, test_cases), 'case_suite': case_suite})
151 
152 
153 # 用例集合页-查看/删除用例
154 @login_required
155 def show_and_delete_case_in_suite(request, suite_id):
156     case_suite = CaseSuite.objects.get(id=suite_id)
157     test_cases = SuiteCase.objects.filter(case_suite=case_suite)
158     if request.method == "POST":
159         test_cases_list = request.POST.getlist('test_cases_list')
160         if test_cases_list:
161             print("勾选用例:", test_cases_list)
162             for test_case in test_cases_list:
163                 test_case = TestCase.objects.get(id=int(test_case))
164                 SuiteCase.objects.filter(case_suite=case_suite, test_case=test_case).first().delete()
165         else:
166             print("测试用例删除失败")
167             return HttpResponse("所选测试用例为空,请选择用例后再进行删除!")
168     case_suite = CaseSuite.objects.get(id=suite_id)
169     return render(request, 'show_and_delete_case_in_suite.html',
170                   {'test_cases': get_paginator(request, test_cases), 'case_suite': case_suite})
171 
172 
173 @login_required
174 def test_case_execute_record(request):
175     test_case_execute_records = TestCaseExecuteResult.objects.filter().order_by('-id')
176     return render(request, 'test_case_execute_records.html', {'test_case_execute_records': get_paginator(request, test_case_execute_records)})
177 
178 
179 # 默认页的视图函数
180 @login_required
181 def index(request):
182     return render(request, 'index.html')
183 
184 
185 # 登陆页的视图函数
186 def login(request):
187     print("request.session.items(): {}".format(request.session.items()))  # 打印session信息
188     if request.session.get('is_login', None):
189         return redirect('/')
190     # 若是是表单提交行为,则进行登陆校验
191     if request.method == "POST":
192         login_form = UserForm(request.POST)
193         message = "请检查填写的内容!"
194         if login_form.is_valid():
195             username = login_form.cleaned_data['username']
196             password = login_form.cleaned_data['password']
197             try:
198                 # 使用django提供的身份验证功能
199                 user = auth.authenticate(username=username, password=password)  # 从auth_user表中匹配信息,匹配到则返回用户对象
200                 if user is not None:
201                     print("用户【%s】登陆成功" % username)
202                     auth.login(request, user)
203                     request.session['is_login'] = True
204                     # 登陆成功,跳转主页
205                     return redirect('/')
206                 else:
207                     message = "用户名不存在或者密码不正确!"
208             except:
209                 traceback.print_exc()
210                 message = "登陆程序出现异常"
211         # 用户名或密码为空,返回登陆页和错误提示信息
212         else:
213             return render(request, 'login.html', locals())
214     # 不是表单提交,表明只是访问登陆页
215     else:
216         login_form = UserForm()
217         return render(request, 'login.html', locals())
218 
219 
220 # 注册页的视图函数
221 def register(request):
222     return render(request,