本章将正式启动平台后端核心部分的搭建。内容分为两大板块:一是依据第二章的架构设计,调整并完成 Django 项目的目录结构,使其模块化更清晰;二是完成自动化测试功能模块的数据库表设计与创建,这是后续功能开发的核心基础。当这两部分工作完成后,整个项目的骨架便已成型,后续开发将顺畅许多。
术语说明:后文中,“模块”与“APP”同义,均指通过 python manage.py startapp 创建的 Django 应用。
开发前准备
必备基础
为更好地跟随本教程,建议你具备以下基础:
- Python:熟练掌握。
- Django:能够编写基本 Demo。
- Vue3 + Element:能够编写基本 Demo。
当然,即使基础稍有欠缺也不必担心,本文将详细讲解每一个步骤与代码,跟随操作即可完成搭建。
环境准备
请确保你的开发电脑已安装以下软件:
- MySQL 8
- Python 3.6+
- Node.js
- Django 4+
- Pycharm 或 Vscode(用于前端开发)
数据库准备
- 确保 MySQL 8 服务已启动。
- 新建一个名为
apiauto 的数据库。
- 数据库字符集请设置为
utf8mb4,排序规则为 utf8mb4_general_ci。

代码架构调整
回顾前文
根据第二章的设计,我们的后端主要分为三个核心模块:用户、自动化测试和系统管理。项目架构将按照这三个模块进行划分。
在第一章中,我们已创建了 users APP,现在需要新增另外两个 APP。

让我们先回顾第一章结束时的项目目录结构:
apiauto/ # 项目根目录
├─ manage.py # Django 管理脚本
│
├─ apiauto/ # 项目配置目录
│ ├─ __init__.py
│ ├─ settings.py
│ ├─ urls.py
│ ├─ wsgi.py
│ └─ asgi.py
│
└─ users/ # 自定义应用(用户模块)
├─ __init__.py
├─ admin.py
├─ apps.py
├─ models.py
├─ serializers.py
├─ views.py
├─ urls.py
└─ test.py
不难发现,users APP 与项目配置目录 apiauto 处于同一层级。为了更清晰地区分项目配置与业务应用,便于统一管理,我们需要创建一个专门的 apps 目录来存放所有 APP。
新架构目录
计划在同级目录下创建 apps 文件夹,并将 users APP 移入其中。调整后的理想目录结构如下:
apiauto/ # 项目根目录
├─ manage.py # Django 管理脚本
│
├─ apiauto/ # 项目配置目录
│ ├─ __init__.py
│ ├─ settings.py
│ ├─ urls.py
│ ├─ wsgi.py
│ └─ asgi.py
│
└─ apps/ # 应用集中管理目录
├─ users/ # 用户模块APP
├─ auto/ # 自动化测试模块APP(待创建)
└─ sysmanages/ # 系统管理模块APP(待创建)
调整操作步骤
1. 在项目根目录下创建 apps 目录
在 IDE 的项目文件树中,右键点击 apiauto(项目根目录),选择 New -> Directory。

在弹出的对话框中输入 apps,然后按回车确认。

创建成功后,目录结构如下图所示:

2. 将 users APP 移动到 apps 目录下
在 IDE 中,直接将 users 文件夹拖动到 apps 文件夹上,或使用 Move 功能。

系统可能会提示搜索引用,稍等片刻即可完成移动。

移动完成后,目录结构应如下图所示:

让 Django 识别新的 APP 路径
移动 users APP 后,Django 默认的模块查找路径中已不存在原来的 users,因此运行项目会报错:ModuleNotFoundError: No module named 'users'。

解决方法很简单,我们需要修改 apiauto/settings.py 文件,将 apps 目录添加到 Python 的系统路径中。
在 settings.py 文件的开头附近,找到 BASE_DIR 定义行,在其后添加以下代码:
import os, sys
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))

添加这行代码后,项目即可正常运行。

新增功能模块 APP
创建自动化测试模块 APP
现在我们需要在 apps 目录下创建新的 APP。
首先,打开终端,并确保当前工作目录在 apps 下。

输入以下命令创建名为 auto 的 APP(对应自动化测试模块):
python ../manage.py startapp auto

创建系统管理模块 APP
继续在 apps 目录下,输入以下命令创建名为 sysmanage 的 APP:
python ../manage.py startapp sysmanage

至此,平台后端三个核心 APP 已全部创建完毕,项目架构初步形成:
users:负责用户登录、注册、信息维护等所有与用户身份相关的功能。
sysmanage:负责整个系统的后台管理,如权限控制、操作日志、系统配置等。
auto:核心功能模块,负责自动化测试的主体功能,包括环境管理、接口管理、用例管理、测试套件、执行计划等。

有了清晰的架构目录,接下来不可或缺的一步就是数据库表设计。
users 模块的表结构基于 Django 自带的用户模型扩展,此处无需额外设计。
sysmanage 模块主要管理其他模块的数据,自身可能没有独立的业务表,暂时搁置。
- 下面,我们将重点设计
auto 模块的数据库表。
数据库设计
表结构规划
一个完整的接口自动化测试平台,通常需要以下核心数据表:
- 项目表:用于区分不同项目,实现数据隔离。
- 环境配置表:为每个项目配置不同的测试环境(如 SIT、UAT、线上等)。
- 接口树表:以树形结构组织和管理接口,便于按功能模块分类。
- 用例树表:以树形结构组织和管理测试用例。
- 接口表:存储接口的详细定义,可包含 Mock 配置。
- 用例表:存储测试用例的具体步骤、检查点、执行状态等。
- 前置/后置步骤表:记录用例执行前后需要执行的脚本或关键字操作。
- 套件表:测试用例的集合。
- 执行计划表:可关联套件,支持定时任务,用于发起批量测试。
- 用例执行记录表:存储单次用例执行的详细报告。
- 套件执行记录表:存储套件执行的汇总报告。
- 计划执行记录表:存储计划执行的汇总报告。
- 自定义脚本表:存储用户编写的、可在前置后置步骤中调用的脚本。
这些表的实体关系(ER)设计图如下:

核心代码实现
1. 树形结构递归工具
从 ER 图可以看出,“接口树”和“用例树”都是自关联的递归模型。为了方便处理这类数据结构,我们先实现一个通用的树形结构工具类。
在项目根目录下(与 apps 同级)新建一个 utils 目录,并在其中创建 mixins.py 文件,添加以下代码:
class TreeMixin:
"""
给自关联模型提供树形结构功能
"""
def to_dict(self):
"""
转换为 dict 节点
"""
return {
"id": self.id,
"name": self.name,
"children": [child.to_dict() for child in self.children.filter(is_deleted=False)]
}
@classmethod
def build_tree(cls, queryset, parent=None):
"""
构建树形结构 (JSON 风格),自动过滤 is_deleted=True 的节点
"""
nodes = queryset.filter(parent=parent, is_deleted=False)
return [node.to_dict() for node in nodes]
@classmethod
def print_tree(cls, queryset, parent=None, indent=0):
"""
打印缩进树,调试用,自动过滤 is_deleted=True
"""
nodes = queryset.filter(parent=parent, is_deleted=False)
for node in nodes:
print(" " * indent + f"- {node.name}")
cls.print_tree(queryset, node, indent + 2)

这个工具类将为后续所有自递归模型(如接口树、用例树)提供便捷的树形数据转换和展示方法。
2. 编写 models.py
接下来,我们依据 ER 图,在 apps/auto/models.py 中定义所有数据模型。
首先,我们观察到几乎所有表都包含 is_delete(逻辑删除)、created_at、updated_at、created_by 等公共字段。我们可以将这些字段抽取到一个抽象的基类 BaseModel 中。
如果你对 Django 的 ORM 还不熟悉,可以先跟着代码实践,了解模型如何映射为数据库表。核心是掌握字段定义和模型间的关系(ForeignKey, ManyToManyField等)。
以下是完整的 models.py 代码,请结合 ER 图进行理解:
from django.db import models
# Create your models here.
from django.contrib.auth.models import User
# 引用工具树
from utils.mixins import TreeMixin
class BaseModel(models.Model):
"""
抽象基类:给所有表提供通用字段
"""
is_delete = models.BooleanField(default=False, verbose_name="是否删除")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
created_by = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="创建人"
)
class Meta:
abstract = True # 不会生成表,仅供继承
class Project(BaseModel):
"""
项目
"""
pro_name = models.CharField(max_length=100, null=True, unique=True, verbose_name='项目名称,唯一校验')
description = models.TextField(null=True,verbose_name="项目描述")
class Meta:
managed = True
db_table = 'auto_project'
def __str__(self):
return '{id:%d, projectname:%s, description:%s}' \
% (self.id, str(self.projectname), str(self.description))
class Environment(BaseModel):
"""
测试环境配置
"""
name = models.CharField(max_length=50, unique=True, verbose_name="环境名称")
pro = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="test_env", verbose_name="项目其下环境")
base_url = models.URLField(verbose_name="基础 URL")
headers = models.JSONField(blank=True, null=True, verbose_name="公共请求头")
db_config = models.JSONField(blank=True, null=True, verbose_name="数据库配置")
env_params = models.JSONField(blank=True, null=True, verbose_name="环境变量配置")
class Meta:
db_table = "auto_environment" # 表名
verbose_name = "测试环境"
verbose_name_plural = "测试环境"
indexes = [
models.Index(fields=["name"]), # 添加索引
]
def __str__(self):
return self.name
class Apinode(BaseModel, TreeMixin):
"""
接口树,管理接口,此表自我递归,继承TreeMixin的方法
"""
name = models.CharField(max_length=200, null=False, verbose_name='node名称')
parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")
pro = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="api_node", verbose_name="项目下节点")
class Meta:
managed=True
db_table = 'auto_api_node'
def __str__(self):
return self.name
class API(BaseModel):
"""
接口信息表
"""
name = models.CharField(max_length=100, verbose_name="接口名称")
path = models.CharField(max_length=200, verbose_name="接口路径")
method = models.CharField(
max_length=10,
choices=[("GET", "GET"), ("POST", "POST"), ("PUT", "PUT"), ("DELETE", "DELETE"), ("PATCH", "PATCH")],
verbose_name="请求方法"
)
headers = models.JSONField(blank=True, null=True, verbose_name="请求头")
params = models.JSONField(blank=True, null=True, verbose_name="Query 参数")
body = models.JSONField(blank=True, null=True, verbose_name="请求体")
response = models.JSONField(blank=True, null=True, verbose_name='返回结果')
node = models.ForeignKey(Apinode, on_delete=models.DO_NOTHING, related_name="test_api", verbose_name="接口树管理接口")
ismock = models.IntegerField(default=0, null=True, verbose_name='是否mock, 0为否,1为是')
class Meta:
db_table = "auto_api"
verbose_name = "接口"
verbose_name_plural = "接口"
indexes = [
models.Index(fields=["name"]),
models.Index(fields=["method"]),
]
def __str__(self):
return f"{self.name} [{self.method}]"
class Casenode(BaseModel, TreeMixin):
"""
用例树,管理用例,此表自我递归,继承TreeMixin的方法
"""
name = models.CharField(max_length=200, null=False, verbose_name='node名称')
parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")
pro = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="case_node", verbose_name="项目下节点")
class Meta:
managed=True
db_table = 'auto_case_node'
def __str__(self):
return self.name
class TestCase(BaseModel):
"""
测试用例
"""
name = models.CharField(max_length=100, verbose_name="用例名称")
node = models.ForeignKey(Casenode, on_delete=models.DO_NOTHING, related_name="test_case", verbose_name="接口树管理用例")
description = models.TextField(blank=True, null=True, verbose_name="用例描述")
path = models.CharField(max_length=200, verbose_name="接口路径")
method = models.CharField(
max_length=10,
choices=[("GET", "GET"), ("POST", "POST"), ("PUT", "PUT"), ("DELETE", "DELETE"), ("PATCH", "PATCH")],
verbose_name="请求方法"
)
headers = models.JSONField(blank=True, null=True, verbose_name="请求头")
params = models.JSONField(blank=True, null=True, verbose_name="Query 参数")
body = models.JSONField(blank=True, null=True, verbose_name="请求体")
response = models.JSONField(blank=True, null=True, verbose_name='返回结果')
api = models.ForeignKey(API, on_delete=models.DO_NOTHING, related_name="test_cases", verbose_name="关联接口")
checkrestype = models.IntegerField(null=True, verbose_name='检查数据类型 0是返回头,1是返回数据,2是接口状态')
checkmethod = models.CharField(max_length=50, null=True, verbose_name='返回检查方式')
checkdata = models.CharField(max_length=200, null=True, verbose_name='检查期望')
execsort = models.IntegerField(null=True, verbose_name='用例执行排序,在新增时通过获取三级节点下的数量自动生成')
artificial = models.FloatField(null=True, verbose_name='人工执行用时')
status = models.IntegerField(default=0, null=True, verbose_name='执行状态, 1执行中,2执行完成')
class Meta:
db_table = "auto_test_case"
verbose_name = "测试用例"
verbose_name_plural = "测试用例"
indexes = [
models.Index(fields=["name"]),
]
def __str__(self):
return self.name
class Casexkey(BaseModel):
"""
前置、后置执行
"""
case = models.ForeignKey(TestCase, on_delete=models.PROTECT, null=False, related_name="casex_key", verbose_name='用例的id,外键')
beaft = models.IntegerField(null=True, verbose_name='前置或后置,0:前置,1:后置')
method = models.CharField(max_length=200, null=False, verbose_name='执行关键字,执行时会映射到脚本 ')
result = models.CharField(max_length=200, null=True, verbose_name='变量名称')
pars = models.IntegerField(null=True, verbose_name='参数数量')
params1 = models.TextField(null=True, verbose_name='参数1')
params2 = models.TextField(null=True, verbose_name='参数2')
params3 = models.TextField(null=True, verbose_name='参数3')
params4 = models.TextField(null=True, verbose_name='参数4')
params5 = models.TextField(null=True, verbose_name='参数5')
params6 = models.TextField(null=True, verbose_name='参数6')
type1 = models.CharField(max_length=10, null=True, verbose_name='参数一类型')
type2 = models.CharField(max_length=10, null=True, verbose_name='参数二类型')
type3 = models.CharField(max_length=10, null=True, verbose_name='参数三类型')
type4 = models.CharField(max_length=10, null=True, verbose_name='参数四类型')
type5 = models.CharField(max_length=10, null=True, verbose_name='参数五类型')
type6 = models.CharField(max_length=10, null=True, verbose_name='参数六类型')
class Meta:
managed = True
db_table = 'auto_casexec_key'
def __str__(self):
return '公共方法名称:%s\t' % (self.method)
class TestSuite(BaseModel):
"""
测试套件:用例集合
"""
name = models.CharField(max_length=100, verbose_name="套件名称")
description = models.TextField(blank=True, null=True, verbose_name="套件描述")
test_cases = models.ManyToManyField(TestCase, related_name="suites", verbose_name="包含用例")
name = models.CharField(max_length=100, verbose_name="套件名称")
pro= models.ForeignKey(Project, null=True, on_delete=models.DO_NOTHING, related_name="test_suite")
is_driven = models.IntegerField(default=0, null=True, verbose_name='数据驱动判断')
filename = models.CharField(max_length=200, null=True, verbose_name='文件原名称')
savename = models.CharField(max_length=200, null=True, verbose_name='文件存储名称')
artificial = models.FloatField(null=True, verbose_name='人工执行时间')
status = models.IntegerField(default=0, null=True, verbose_name='执行状态,0待执行,1执行中')
class Meta:
db_table = "auto_test_suite"
verbose_name = "测试套件"
verbose_name_plural = "测试套件"
def __str__(self):
return self.name
class TestPlan(BaseModel):
"""
执行计划
"""
name = models.CharField(max_length=100, verbose_name="计划名称")
environment = models.ForeignKey(Environment, on_delete=models.SET_NULL, null=True, related_name="test_plan", verbose_name="执行环境")
description = models.TextField(blank=True, null=True, verbose_name="计划描述")
suite = models.ForeignKey(TestSuite, on_delete=models.CASCADE, related_name="test_plan", verbose_name="关联套件")
schedule = models.CharField(max_length=50, blank=True, null=True, verbose_name="定时任务表达式")
qymsg = models.IntegerField(null=True, help_text='0,不发送消息, 1,发送消息')
webhook = models.CharField(max_length=500, null=True, help_text='企业微信群机器人webhook')
class Meta:
db_table = "auto_test_plan"
verbose_name = "执行计划"
verbose_name_plural = "执行计划"
def __str__(self):
return self.name
class Script(BaseModel):
"""
脚本(前置/后置、SQL/Python)
"""
kw_name = models.CharField(max_length=50, null=True, unique=True, verbose_name='脚本文件名称')
script_type = models.CharField(
max_length=20,
choices=[("PYTHON", "Python Script"), ("SQL", "SQL Script")],
verbose_name="脚本类型"
)
methed_name = models.CharField(max_length=200, null=True, unique=True,
verbose_name='脚本里面可以多个函数,取其中一个作入口函数名称,唯一')
content = models.TextField(null=True, verbose_name='脚本内容 ')
pars = models.IntegerField(null=True, verbose_name='参数数量')
rules = models.TextField(null=True, verbose_name='规则描述,备注')
public = models.IntegerField(default=0, verbose_name="以项目为维度,0:为私有,1:为公开")
pro = models.ForeignKey(Project, null=True, on_delete=models.DO_NOTHING, related_name="script",)
class Meta:
db_table = "auto_script"
verbose_name = "脚本"
verbose_name_plural = "脚本"
def __str__(self):
return f"{self.kw_name} ({self.script_type})"
class TestPlanResult(BaseModel):
"""
计划执行记录
"""
plan = models.ForeignKey(TestPlan, null=True, on_delete=models.DO_NOTHING, related_name="plan_result", verbose_name='执行组ID')
tp_name = models.CharField(max_length=255, null=True, verbose_name='执行计划名称')
pro = models.ForeignKey(Project, null=True, on_delete=models.DO_NOTHING, related_name="plan_result", verbose_name='项目ID')
class Meta:
managed=True
db_table = 'auto_test_plan_result'
def __str__(self):
return '名称%s' % (str(self.tp_name))
class TestSuiteResult(BaseModel):
"""
测试套件执行报告
"""
suite = models.ForeignKey(TestSuite, null=True, on_delete=models.DO_NOTHING, related_name="suites_result")
excutnum = models.CharField(max_length=200, null=False, verbose_name='执行批次编号,按计划+用例生成数据,用此字段判断报告文件集,有多个相同编号的数据组成一封报告')
planName = models.CharField(max_length=200, null=True, verbose_name='计划名称')
cases = models.IntegerField(null=True, verbose_name='用例总数')
passs = models.IntegerField(null=True, verbose_name='通过数量')
fails = models.IntegerField(null=True, verbose_name='失败数量')
totaltime = models.CharField(max_length=100, null=True, verbose_name='计划执行总用时')
sCount = models.IntegerField(null=True, verbose_name='300ms<=s<700ms的数量')
nCount = models.IntegerField(null=True, verbose_name='700ms<=s<1000ms的数量')
cCount = models.IntegerField(null=True, verbose_name='1000ms<=s的数量')
pro = models.ForeignKey(Project, null=True, on_delete=models.DO_NOTHING, verbose_name='项目id')
tpr = models.ForeignKey(TestPlanResult, null=True, on_delete=models.DO_NOTHING, related_name="suites_result", verbose_name='套件的id')
artificial = models.FloatField(null=True, verbose_name='计划执行时人工用时')
savetime = models.FloatField(null=True, verbose_name='比人工省时')
plancname = models.CharField(max_length=50, null=True, verbose_name='计划创建人名称')
execuname = models.CharField(max_length=50, null=True, verbose_name='报告创建人名称')
status = models.IntegerField(default=0, null=True, verbose_name='执行状态,0:执行完成 ,1:执行中')
class Meta:
managed=True
db_table = 'auto_test_suite_result'
def __str__(self):
return '编号%s' % (str(self.excutnum))
class TestResult(BaseModel):
"""
用例执行结果
"""
casename = models.CharField(max_length=100, null=True, verbose_name="存放用例名称")
excutnum = models.CharField(max_length=200, null=False,
verbose_name='执行批次编号,按计划+用例生成数据,用此字段判断报告文件集,有多个相同编号的数据组成一封报告')
loopnum = models.CharField(max_length=200, null=True, verbose_name='执行循环的编号')
test_plan = models.ForeignKey(TestPlan, null=True, on_delete=models.DO_NOTHING, related_name="case_result", verbose_name="关联执行计划,可为空,用例可单独执行")
test_case = models.ForeignKey(TestCase, on_delete=models.DO_NOTHING, related_name="case_result", verbose_name="关联用例")
status = models.CharField(
max_length=20,
choices=[("PASS", "Pass"), ("FAIL", "Fail")],
verbose_name="执行结果"
)
response_data = models.JSONField(blank=True, null=True, verbose_name="请求信息与响应数据,这里有数据性能瓶颈,大家遇到再自行解决")
duration = models.FloatField(verbose_name="耗时(秒)")
driven = models.IntegerField(null=True, default=0, verbose_name='是否数据驱动,0:不驱动, 1:数据驱动')
parentid = models.IntegerField(null=True, default=-1, verbose_name='驱动第一条数据id')
exectime = models.FloatField(null=True, verbose_name='用例执行用时')
savetime = models.FloatField(null=True, verbose_name='比人工省时')
class Meta:
db_table = "auto_test_result"
verbose_name = "测试结果"
verbose_name_plural = "测试结果"
indexes = [
models.Index(fields=["status"]),
]
def __str__(self):
return f"{self.test_case.name} - {self.status}"
class BeaftResult(BaseModel):
"""
前置、后置执行结果
"""
id = models.AutoField(primary_key=True)
casename = models.CharField(max_length=100, null=True, verbose_name="存放用例名称")
status = models.IntegerField(null=False, verbose_name='执行结果状态 ,1是成功,0是失败')
elapsedt = models.CharField(max_length=100, null=True, verbose_name='请求响应时间')
qutoe_case_type = models.IntegerField(null=True, default=0, verbose_name='0前置,1后置')
qutoe_case_execorder = models.IntegerField(default=0, null=True, verbose_name='自定义方法前后执行:0 前, 1 后')
tr = models.ForeignKey(TestResult, null=True, on_delete=models.DO_NOTHING, related_name="beaft_result", verbose_name='用例测试报告外键')
class Meta:
managed = True
db_table = 'auto_beaft_result'
3. 生成数据库表
完成 models.py 的编写后(除了抽象基类 BaseModel),我们需要通过 Django 的迁移命令,在 MySQL 数据库中创建对应的表。
在项目根目录下执行以下命令:
# 1. 创建迁移文件:根据 models.py 的改动生成迁移脚本
python manage.py makemigrations
# 2. 应用迁移:执行迁移脚本,在数据库中创建或修改表结构
python manage.py migrate
说明:makemigrations 命令会检测模型变化并生成记录文件;migrate 命令则负责执行这些记录,将其同步到数据库。如果你是首次为 auto 应用生成表,makemigrations 会创建对应的初始迁移文件。
至此,一个功能完备的自动化测试平台所需的 软件测试 数据表就设计并创建完成了。
总结
本章完成了从零搭建自动化测试平台中颇具挑战性的两部分工作:清晰的后端代码架构划分和详尽的业务数据库设计。我们调整了项目目录,将应用统一管理在 apps 下;并设计了涵盖项目、环境、接口、用例、计划、报告等全流程的核心数据模型。
骨架已然搭好,接下来的章节我们将正式进入代码编写阶段,开始实现项目管理、环境管理等前端页面,并逐步完善平台的各项功能。如果你在搭建过程中遇到问题,欢迎在技术社区交流探讨。真正的“造轮子”之旅,现在才正式开始!