该文原创为新潮质量保障技术团队中的 “上进的中年软件测试从业者”,用于技术交流分享
思路清晰的人前途各有不同;不清晰只有一种后果,随波逐流。以我个人举例,最早 12 年就开始使用二次封装过的 Eclipse 作为公司的自动化,当时到了 14 年要被公司因为业务问题裁员的时候都不是知道那是大名鼎鼎的 Eclipse,当时还是自动化组的负责人,写过很多 python 脚本。现在回头想想简直抠脚,同样颠沛流离了好几年。过往的经历后面会穿插介绍。
目前从大厂出来的,基本上都有测试平台的经历,也会提议所在的测试部门有自己的测试平台。慢慢的听多了,自己也会想尝试去搭建一套。由头有了,接下来就是干了,之前短暂的接触过 flask_admin 作为 python web 开发的框架。所以想也没想就用了这个框架。(下图是我们测试平台的首页,背景是借鉴别人的,主要是高大上)
当前这套框架的组成为flask_admin、Apscheduler、MongoEngine、Echart、ThreadPoolExecutor等。数据库包括mysql、mongo、redis(选用) 等。其他相关联工具包括Jenkins、JIRA、Testlink等。
flask_admin: 选择 flask_admin 的原因,首先有个人原因,先入为主;其次对于 web 前端有与生俱来的恐惧感,真的调不来, 而 flask_admin 能让你把大多数的精力放在后端的逻辑上面。Apscheduler 作为定时任务的框架引入,具体的实战过程后续介绍。
MongoEngine:这里介绍一下选用 MongoEngine 而不用 pyMongo 的原因,pymongo 和 mongoEngine 都是 flask 可选的 mongo 数据库引擎,但是我们选用 mongo 数据库本身的原因就是想应对复杂数据的操作,而 mongoEngine 对于如 list, dict 这一类的数据处理简直无懈可击,如 ReferenceField、ListField、EmbeddedDocument 以及 DictField 等,重点是 flask_admin 对于 mongoEngine 的支持虽然存在瑕疵(后续介绍),但是真的已经非常好了,如下图对列表和字典的结合处理。(如下代码及实现效果)
class Header(EmbeddedDocument):
header_key = StringField(db_field="header_key")
header_value = StringField(db_field="header_value")
header_rule = DictField(db_field="header_rule", description=u"规则",
default={
"header1": {"h_optional": False, "h_check": True, "h_type": "integer", "h_range": [],
"h_len_min": 0, "h_len_max": 0},
"header2": {"h_optional": False, "h_check": True, "h_type": "integer", "h_range": [],
"h_len_min": 0, "h_len_max": 0}})
class Data(EmbeddedDocument):
data_key = StringField(db_field="data_key")
data_value = StringField(db_field="data_value")
data_rule = DictField(db_field="data_rule", description=u"规则",
default={"body1": {"b_default": "", "b_optional": False, "b_check": True, "b_type": "integer",
"b_range": [],
"b_len_min": 0, "b_len_max": 0},
"body2": {"b_default": "", "b_optional": False, "b_check": True, "b_type": "integer",
"b_range": [],
"b_len_min": 0, "b_len_max": 0}})
class Asset(EmbeddedDocument):
asset_key = StringField(db_field="asset_key")
asset_value = StringField(db_field="asset_value")
class InterfaceTestCaseForm(Document):
project = ReferenceField(ProjectForm)
creator = ReferenceField(TesterForm)
updater = ReferenceField(TesterForm)
interface = StringField(db_field="interface", description=u"接口地址",
validators=[DataRequired(message=u"接口地址不能为空")], max_length=100)
name = StringField(db_field="name", description=u"接口名称",
validators=[DataRequired(message=u"接口名称不能为空")], max_length=50)
interfaceType = StringField(db_field="interfaceType", description=u"接口请求类型",
validators=[DataRequired(message=u"接口请求类型不能为空")], max_length=50)
testPlanType = ReferenceField(TestPlanTypeForm, db_field="testPlanType")
internal = BooleanField(db_field="internal", default=True)
smoke = BooleanField(db_field="smoke", default=True)
regression = BooleanField(db_field="regression", default=True)
testResult = StringField(db_field="testResult")
headers = ListField(
EmbeddedDocumentField(Header),
max_length=30)
data = ListField(
EmbeddedDocumentField(Data),
max_length=30)
asset = ListField(
EmbeddedDocumentField(Asset),
max_length=30)
createTime = StringField(db_field='createTime', max_length=20) # TODO 修改改成DateField格式,参照TEST-57
updateTime = StringField(db_field='updateTime', max_length=20) # TODO 修改改成DateField格式,参照TEST-57
deleted = BooleanField(db_field="deleted", default=False)
meta = {
"collection": "interfaceTestCase",
'ordering': ['-updateTime']
}
class ProjectForm(Document):
projectName = StringField(db_field="projectName", validators=[DataRequired(message=u"项目名不能为空")], )
projectPrefix = StringField(db_field="projectPrefix", validators=[DataRequired(message=u"项目前缀,请与testlink保持一致")])
projectTestOwner = ReferenceField(TesterForm)
projectTesters = ListField(ReferenceField(TesterForm)) # 增加项目测试人员
projectDomain = StringField(db_field="projectDomain", validators=[DataRequired(message=u"项目域名不能为空")], )
projectAuthUrl = StringField(db_field="projectAuthUrl", validators=[DataRequired(message=u"系统登录地址不能为空")], )
authUser = StringField(db_field="authUser", validators=[DataRequired(message=u"系统默认登录用户名不能为空")], )
authPassword = StringField(db_field="authPassword", validators=[DataRequired(message=u"系统默认登录密码不能为空")], )
projectDescription = StringField(db_field="projectDescription", validators=[DataRequired(message=u"项目描述不能为空")])
meta = {
"collection": "projects"
}
def __unicode__(self): # 解决对象引用过程中,前端看到的是可视化的名字,而不是对象名称。
return self.projectName
mysql:如数据业务简单但是存在各种耦合关联,避免脏数据产生等,mysql 可以在数据库和表层面做很多限制工作,让程序运行更平稳,但是初始化或者前期工作就需要做很多,我们的平台基本上没有用。这个我用 mysql 的唯一原因就是保留flask_admin 对 mysql 支持的尊重。
Redis:这里我没有选择 Redis 的原因只有一个,让平台简洁化。Redis 能够实现的功能,如内存存储、锁,完全可以用 Mongo 和环境变量来替代。这里要讲一下 Redis 能够作为锁的主要原因是 Redis 是单进程的,可以创建一个对象,但是同一时间只有一个进程操作这个对象,也就实现了锁的功效。实战过程中会教大家如何用环境变量来实现锁的功能。
Jenkins:我面试过很多人,对 Jenkins 的了解都是停留在部署、执行自动化任务这些常规的功能。殊不知,Jenkins 能实现的功能完全超乎你的想象,我曾经用 Jenkins 实现了一个外包人员的考核系统,用于考核外包人员对测试技能的掌握程度,这个功能的实现让我兴奋了一段时间。在这里,我用 Jenkins 主要是为了解决容器化部署带来的容器基础功能不完善以及测试平台一键部署问题。
JIRA:JIRA 作为常规的缺陷管理工具,自然成为测试平台需要集成的工具,包括各种数据报表的展示等。
Testlink:Testlink 作为用例管理工具的特点,轻量、操作方便、可以关联 JIRA、域账号管理,这也是不用禅道的原因,当然禅道的本身的定位以及扩展功能的收费,也是我们奋战在各种绿色和破解版的这些人所不能接受的。
以上就是对测试平台选型及框架工具的介绍,欢迎轻拍,重拍绝交。下一期将介绍测试平台的准备工作。下期见!