前言#
在去年的年中,我一时冲动写了一个基于flask开发的测试平台,然后把服务托管在了腾讯云上,本来想是写文章分享的怎么开发的,但是一直没有写。如今一年已经过去了,服务器也是到期了,演示网址也是无法访问了。
首先肯定有一个名称,因为我羡慕自由的鸟儿飞翔在天空,所以我给起的名字是flytest
。好了,名称就是这个样子了。继续看吧。
技术栈#
本项目是一个前后端不分离的jinja2渲染形式的平台,所以前端页面我使用的是bootstrap4。别问我为啥不用VUE,问就是不会。
环境 | 用途 |
bootstrap4 | 页面显示 |
flask | 后端使用的框架 |
celery | 测试用例执行 |
MySQL | 测试数据的存储 |
redis | 缓存 |
APScheduler | 定时任务开发 |
nginx+gunicorn+supervisor | 环境部署 |
平台结构#
测试平台大概分为这么几个模块,每一个模块都有相关的作用,但是每个模块之间又有着很强的关联性。
模块 | 作用 |
项目管理 | 对不同项目进行管理 |
环境管理 | 管理测试环境和生产环境 |
测试管理 | 添加用例并进行测试 |
任务列表 | 测试的任务和任务结果显示 |
定时任务 | 定时任务列表 |
最新报告 | 显示最近的测试结果 |
问题管理 | 显示最新的问题 |
结果趋势 | 通过折线图查看最新的测试情况 |
平台截图#
注册页面
主界面
测试管理页
流程设计#
以下是我对于这个测试平台的流程构想,其中人员管理部分是采用邮箱注册并激活的形式进行处理。
数据库设计#
在这个模型文件中使用了两个flask第三方库,分别是flask-login
和flask-avatars
。
# 需要做这样的导入
from flask_login import UserMixin, current_user
from flask_avatars import Identicon
用户表
字段名称 | 类型 | 解释 | 关系 |
id | Integer | 主键 | |
email | Char(128) | 邮箱地址 | |
username | Char(128) | 用户名称 | |
password_hash | Char(128) | 密码 | |
avatar_s | Char(64) | 头像 | |
avatar_m | Char(64) | 头像 | |
avatar_l | Char(64) | 头像 | |
is_acticed | Boolean | 是否激活 | |
product | 关联项目表 | ||
apitest | 关联测试表 |
产品管理表
字段名称 | 类型 | 解释 | 关系 |
name | Char(128) | 产品名称 | |
desc | Char(512) | 产品描述 | |
tag | Char | 产品类型 | |
user_id | Integer | 产品负责人 |
测试地址管理表
字段名称 | 类型 | 解释 | 关系 |
name | Char(128) | 地址名称 | |
url | Char(512) | 地址 | |
product_id | Integer | 产品ID |
测试表
字段名称 | 类型 | 解释 | 关系 |
name | Char(128) | 地址名称 | |
results | Integer | 测试结果 | |
task_id | Char(256) | 任务ID | |
user_id | Integer | 用户ID |
测试步骤
字段名称 | 类型 | 解释 | 关系 |
name | Char(128) | 请求名称 | |
method | Char(16) | 请求方法 | |
route | Char(512) | 请求路径 | |
headers | Text | 请求头 | |
request_data | Text | 请求数据 | |
expected_result | Char(512) | 预期值 | |
expected_regular | Char(512) | 预期正则 | |
request_extract | Char(512) | 请求验证 | |
response_extract | Char(512) | 返回验证 | |
results | Text(2048) | 结果 |
报告
字段名称 | 类型 | 解释 | 关系 |
types | Integer | 1普通任务2定时任务 | |
task_id | Char(256) | 任务ID | |
name | Char(256) | 名称 | |
result | Char(2048) | 结果 |
问题库
字段名称 | 类型 | 解释 | 关系 |
task_id | Char(256) | 任务ID | |
casename | Char(256) | 用例名称 | |
stepname | Char(512) | 步骤名称 | |
request | Text | 请求 | |
detail | Text | 详情 | |
level | Char(10) | 级别 |
任务结果
字段名称 | 类型 | 解释 | 关系 |
task_id | Char(256) | 任务ID | |
name | Char(512) | 任务名称 | |
hostname | Char(512) | 主机名称 | |
params | Char(512) | 任务参数 | |
result | Text | 结果 | |
traceback | Text | 异常详情 |
定时任务
字段名称 | 类型 | 解释 | 关系 |
task_id | Char(256) | 任务ID | |
test_name | Char(128) | 任务名称 | |
func_name | Char(256) | 任务执行函数名 | |
trigger | Char(64) | 执行类型 | |
args | Char(128) | 参数 | |
kwargs | Char(128) | 关键字参数 | |
max_instances | |||
times | |||
misfire_grace_time | |||
next_run_time | Datetime | 下次运行时间 | |
start_date | Date | 开始日期 | |
is_active | Boolean | 是否激活 | |
product_id | Integer | 项目ID |
以上我们把表结构大概设计好了,看看实际的sqlalchemy模型文件吧:
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask import session, current_app
from flask_login import UserMixin, current_user
from flask_avatars import Identicon
from app.extensions import db, cache
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(128), unique=True)
username = db.Column(db.String(128))
password_hash = db.Column(db.String(128))
avatar_s = db.Column(db.String(64))
avatar_m = db.Column(db.String(64))
avatar_l = db.Column(db.String(64))
created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
updated = db.Column(db.DateTime, onupdate=datetime.utcnow,
default=datetime.utcnow)
is_deleted = db.Column(db.Boolean, default=False, nullable=False)
is_active = db.Column(db.Boolean, default=False, nullable=False)
products = db.relationship('Product', back_populates='user')
apitests = db.relationship('Apitest', back_populates='user')
def __init__(self, *args, **kwargs):
super(User, self).__init__(*args, **kwargs)
self.generate_avatar()
def generate_avatar(self):
avatar = Identicon()
filenames = avatar.generate(text=self.email)
self.avatar_s = filenames[0]
self.avatar_m = filenames[1]
self.avatar_l = filenames[2]
db.session.commit()
@property
def password(self):
return AttributeError("password is only readable")
@password.setter
def password(self, pwd):
self.password_hash = generate_password_hash(pwd)
def verify_password(self, pwd):
return check_password_hash(self.password_hash, pwd)
def __repr__(self):
return self.username
def __str__(self) -> str:
return self.username
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128))
desc = db.Column(db.Text)
tag = db.Column(db.String(32))
created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
updated = db.Column(db.DateTime, onupdate=datetime.utcnow,
default=datetime.utcnow)
is_deleted = db.Column(db.Boolean, default=False, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
user = db.relationship('User', back_populates='products')
apiurls = db.relationship('Apiurl', back_populates='product')
apitests = db.relationship('Apitest', back_populates='product')
reports = db.relationship('Report', back_populates='product')
bugs = db.relationship('Bug', back_populates='product')
works = db.relationship('Work', back_populates='product')
def __repr__(self):
return '<Product %s>' % self.name
@staticmethod
def get_product(pk):
if pk is not None:
session["current_product"] = pk
pk = session.get("current_product", None)
product = Product.query.filter_by(id=pk, user=current_user, is_deleted=False).one_or_none() or Product.query.filter_by(user=current_user, is_deleted=False).first()
return product
class Apiurl(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128))
url = db.Column(db.String(512))
created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
updated = db.Column(db.DateTime, onupdate=datetime.utcnow,
default=datetime.utcnow)
is_deleted = db.Column(db.Boolean, default=False, nullable=False)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
product = db.relationship('Product', back_populates="apiurls")
apisteps = db.relationship('Apistep', back_populates='apiurl')
def __repr__(self):
return '<Url %s>' % self.name
class Apitest(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64))
results = db.Column(db.Integer, default=-1)
task_id = db.Column(db.String(255), index=True)
created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
updated = db.Column(db.DateTime, onupdate=datetime.utcnow,
default=datetime.utcnow)
is_deleted = db.Column(db.Boolean, default=False, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
user = db.relationship('User', back_populates='apitests')
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
product = db.relationship('Product', back_populates='apitests')
apisteps = db.relationship('Apistep', back_populates='apitest')
def __repr__(self):
return '<ApiTest %s>' % self.name
class Apistep(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128))
method = db.Column(db.String(16))
route = db.Column(db.String(512))
headers = db.Column(db.Text)
request_data = db.Column(db.Text, nullable=True)
expected_result = db.Column(db.String(512))
expected_regular = db.Column(db.String(512), nullable=True)
request_extract = db.Column(db.String(512))
response_extract = db.Column(db.String(512))
status = db.Column(db.Integer, default=-1)
results = db.Column(db.Text(2048), nullable=True)
created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
updated = db.Column(db.DateTime, onupdate=datetime.utcnow,
default=datetime.utcnow)
is_deleted = db.Column(db.Boolean, default=False, nullable=False)
apiurl_id = db.Column(db.Integer, db.ForeignKey('apiurl.id'))
apiurl = db.relationship('Apiurl', back_populates='apisteps')
apitest_id = db.Column(db.Integer, db.ForeignKey('apitest.id'))
apitest = db.relationship('Apitest', back_populates='apisteps')
report_id = db.Column(db.Integer, db.ForeignKey("report.id"))
report = db.relationship('Report')
def __repr__(self):
return '<ApiStep %s>' % self.name
class Report(db.Model):
id = db.Column(db.Integer, primary_key=True)
types = db.Column(db.Integer, default=1) # 1是普通任务 2是定时任务
task_id = db.Column(db.String(256), index=True)
name = db.Column(db.String(256), default="NULL")
result = db.Column(db.String(2048))
status = db.Column(db.Integer, default=-1)
created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
updated = db.Column(db.DateTime, onupdate=datetime.utcnow,
default=datetime.utcnow)
is_deleted = db.Column(db.Boolean, default=False, nullable=False)
apistep = db.relationship('Apistep', uselist=False)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
product = db.relationship('Product', back_populates='reports')
class Bug(db.Model):
id = db.Column(db.Integer, primary_key=True)
task_id = db.Column(db.String(256), index=True)
casename = db.Column(db.String(256))
stepname = db.Column(db.String(512))
request = db.Column(db.Text)
detail = db.Column(db.Text)
status = db.Column(db.Integer)
level = db.Column(db.String(10), default='一般')
created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
updated = db.Column(db.DateTime, onupdate=datetime.utcnow,
default=datetime.utcnow)
is_deleted = db.Column(db.Boolean, default=False, nullable=False)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
product = db.relationship('Product', back_populates='bugs')
def __repr__(self):
return '<BUG FOR %S>' % self.stepname
class Work(db.Model):
id = db.Column(db.Integer, primary_key=True)
task_id = db.Column(db.String(256), index=True)
name = db.Column(db.String(512))
hostname = db.Column(db.String(512))
params = db.Column(db.String(512))
status = db.Column(db.Text)
result = db.Column(db.Text)
traceback = db.Column(db.Text)
created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
product = db.relationship('Product', back_populates='works')
def __repr__(self):
return self.task_id
class CronTabTask(db.Model):
"""定时任务"""
id = db.Column(db.Integer, primary_key=True)
task_id = db.Column(db.String(256), index=True)
test_name = db.Column(db.String(128))
func_name = db.Column(db.String(256))
trigger = db.Column(db.String(64))
args = db.Column(db.String(128))
kwargs = db.Column(db.String(128))
max_instances = db.Column(db.Integer)
times = db.Column(db.String(128))
misfire_grace_time = db.Column(db.Integer)
next_run_time = db.Column(db.String(256))
start_date = db.Column(db.String(256))
is_active = db.Column(db.Boolean, default=True, nullable=False)
product_id = db.Column(db.Integer)
以上就是模型文件的所有数据了,感觉第一篇应该讲一下怎么搭建环境,好了,先写这么多吧。