一段痛苦的日子
从开始负责小程序开始,基本每两周发一次版本,并且是在10点半后开始发版。经常遇到的场景是等到10点,以为测试的差不多了,没有什么问题。
啪啪,打脸来的太快,总会发现一些问题,或是本次迭代导致的,或是之前遗留的,怎么办总不能带着问题上线吧。
“改”,此时回家又变成了“不是在此时 不知在何时”!
这就是我想要去做小程序自动化测试的初衷,通过自动化的方式大幅提升测试效率,尤其是回归测试的效率,提高测试覆盖率,避免漏测。
自动化测试方案选型
最终技术选型是 Minium,为什么选这个可以参考之前写的小程序自动化测试-选型这篇文章。
这里再补充三条为什么要使用脚本自动化测试的方案,而不是其他方案,毕竟脚本自动化测试还需要编码。
这样考虑是基于我们的项目特点:
- 需求变动不频繁
我们的项目是类app的小程序,页面和功能很多,对单个功能来说需求变动不频繁,这样测试脚本不用频繁变更。
- 项目周期足够长
长期和app需求同步开发,项目周期足够长,这样测试脚本也就可以一点点丰富。
- 对每个迭代来说,测试脚本可以重复利用。
目标
那么做到什么程度,才能解决上边提到的痛点了(对,痛点就是要能早回家😜)。
想象下最合理的脚本自动化测试应该是什么样的?
针对某个功能的测试,可能的操作需要经过多个页面、多个按钮、多块功能,到最后需要验证最终结果是否和预期一致。
流程类似这样:
我希望我们的测试或研发只需要编写一个配置文件,录入需要走的页面,点击的按钮或页面操作,最终需要校验的内容,上传运行配置,就可以做到一个特定流程的自动化测试。
最终一些配置也可以固化下来,因为很多功能不常变,每次回归不但测试脚本可以复用,甚至配置也可以复用。
一口气吃个胖子是不现实的,我们把愿景分为了三个阶段目标:
- 跑通主流程测试,验证技术方案
目的:对于不常变的功能,做到脚本自动化测试
- 搭建Mock系统,基于Mock进行测试
特定的场景需要特定的数据,基于mock可以做接口容灾,功能一致性测试(固定的数据对应固定的展示)。
- 测试用例支持配置化
主要目的:可通过配置,无代码完成测试用例执行
架构设计
除了最下边的依赖层外,整个方案包含三层:
一、底层服务层
包含路由跳转、utils、mock等底层服务,还有常用的底层业务功能,例如登录、定位等。
二、页面功能Case
这一层主要是针对每个页面的基本操作和校验,例如在搜索页通过测试脚本点击搜索按钮、点击搜索历史、自动输入搜索词、校验搜索内容展示等功能。可以说这一层是把页面可继续的操作通过脚本方案暴漏出来。
以搜索页为例,会提供如下方法:
# 选中搜索输入框
def search_focus(self):
# 打印过程
process_print('选中搜索输入框')
search_input = self.get_element('input', max_timeout=10)
search_input.tap()
self.wait_for(2)
# 每一步操作截图,方便人工校验
self.capture('搜索框聚焦')
# 进行搜索
def search_input(self, text):
process_print('搜索文本:', text)
search_input = self.get_element('input', max_timeout=10)
search_input.input(text)
self.wait_for(1)
self.capture('搜索框输入')
# 检查热门搜索展示数量是否正确
def check_show_hot_word(self):
process_print('检查热门搜索展示数量是否正确')
self.wait_for(10)
search_bar = self.get_search_bar()
theme_suggest = search_bar.data.themeSuggest
current_index = int(search_bar.data.currentIndex)
hot_words_data = theme_suggest[current_index]
if len(theme_suggest) > 0:
theme_element = search_bar.get_elements('.xxx', max_timeout=3)[current_index]
hot_word_elements = theme_element.get_elements('.xxx-text')
self.mini.assertEqual(len(hot_word_elements), len(hot_words_data))
三、流程测试:
这一层就是具体的测试用例,比如上边提到的首页加购流程,过程经过多个页面,在这个测试流程中,我们可以通过调用登录、首页、购物车页提供的各个方法,通过代码完成加购的流程。
最后校验结果是否和我们预期的一致。
一条简单的搜索测试用例:
# 进入地址页
self.home.enter_address()
self.home.ready()
# 选择城市
self.address.enter_select_city()
self.address.ready()
self.address.select_city('上海')
self.address.focus()
self.address.input('东方明珠')
self.address.select_address()
# 加载数据
self.home.load()
# 点击进入搜索页
self.home.search_btn_click()
# 搜索结果翻页
self.page.wait_for(10)
self.search.search_input('水')
self.search.search_word_click(0)
self.page.wait_for(5)
self.search.scroll_result(1000)
# 校验功能是否存在
self.search.check_instant_delivery()
数据Mock
在整个脚本自动化测试中,数据Mock是重要的一环。
很多场景下我们需要脚本运行时使用的数据和我们期望一致,这样才能保证特定的功能验证通过。 否则数据不一致,展示也不一致,这是就需要用到Mock。
Minium支持对 wx.request、wx.getStorageSync 等方法进行Mock,这样就可以拦截,替换我们准备的数据。
由于我们既想Mock,还行进行数据录制,所以并没有使用 mock_wx_method() 进行mock,而是使用 evaluate() 来进行代码注入。
通过 evaluate() 可以向小程序 app Service 层注入JS代码并执行。
Mock的方式有两种:
- 通过替换URL,把接口地址替换为YAPI的Mock数据地址
- 通过替换返回数据,返回本地的 mock 数据
前文提到数据录制,主要目的是为了解决,当需要使用大量本地 Mock 数据的时候,全部靠人工仿造数据太过麻烦。
于是我们想到在脚本运行期间,访问测试或线上环境的接口,拦截并把数据保存在本地。
这样我们只需要手动修改部分数据,即可 Mock 整个测试流程的数据。
大体流程如下:
通过 evaluate() 注入的代码如下:
# 覆盖wx.request 对请求及响应进行拦截,实现mock, yapi功能
def mock_request(mini: minium.MiniTest, mock_data: dict):
mini.app.evaluate(
"""function () {
if (!wx.$$request) {
wx.$$request = true
const request = wx.request
const mockData = %(mock_data)
wx.mock_response = function(data) {
return {
statusCode: 200,
data: {
code: 0,
data,
now: Date.now() - 2,
}
}
}
const http_proxy = %(proxy)s
wx.request = function(original_option) {
let option = {...original_option}
http_proxy(option, new LocalURL(option.url))
const mock = getMock(option.url)
const handle = (method, payload) => {
if (typeof option[method] === 'function') {
option[method](payload)
}
// 省略代码...
}
}
}
}""" % (dict(
mock_data = json_to_string(mock_data),
proxy = http_proxy
)),
sync = True
)
总结
通过上述方案结合 Jenkins 等工具,可以配置定期执行测试脚本。
可以编写一些常见模块的测试用例,在回归阶段定期执行,输出测试报告。也可以通过半人工的方式,检测执行过程中的截图,判断 UI 展示是否正常。
这样比起人员每个页面、每个模块的点击,效率会提升不少。
同时在基于 Mock 还可以做接口容灾测试等测试。
因为每个页面的操作已经函数化,可以把 “流程测试”中的代码变成配置化,测试人员可以通过配置要走的页面、需要点击的模块、等待时长等完成测试用例的脚步配置。