微信小程序脚本自动化测试方案

1,157 阅读6分钟

一段痛苦的日子

从开始负责小程序开始,基本每两周发一次版本,并且是在10点半后开始发版。经常遇到的场景是等到10点,以为测试的差不多了,没有什么问题。
啪啪,打脸来的太快,总会发现一些问题,或是本次迭代导致的,或是之前遗留的,怎么办总不能带着问题上线吧。

“改”,此时回家又变成了“不是在此时 不知在何时”!

这就是我想要去做小程序自动化测试的初衷,通过自动化的方式大幅提升测试效率,尤其是回归测试的效率,提高测试覆盖率,避免漏测。

自动化测试方案选型

最终技术选型是 Minium,为什么选这个可以参考之前写的小程序自动化测试-选型这篇文章。

这里再补充三条为什么要使用脚本自动化测试的方案,而不是其他方案,毕竟脚本自动化测试还需要编码。
这样考虑是基于我们的项目特点:

  1. 需求变动不频繁

    我们的项目是类app的小程序,页面和功能很多,对单个功能来说需求变动不频繁,这样测试脚本不用频繁变更。

  2. 项目周期足够长

    长期和app需求同步开发,项目周期足够长,这样测试脚本也就可以一点点丰富。

  3. 对每个迭代来说,测试脚本可以重复利用。

目标

那么做到什么程度,才能解决上边提到的痛点了(对,痛点就是要能早回家😜)。

想象下最合理的脚本自动化测试应该是什么样的?

针对某个功能的测试,可能的操作需要经过多个页面、多个按钮、多块功能,到最后需要验证最终结果是否和预期一致。

流程类似这样:

Pasted image 20240229170359.png

我希望我们的测试或研发只需要编写一个配置文件,录入需要走的页面,点击的按钮或页面操作,最终需要校验的内容,上传运行配置,就可以做到一个特定流程的自动化测试。

最终一些配置也可以固化下来,因为很多功能不常变,每次回归不但测试脚本可以复用,甚至配置也可以复用。

一口气吃个胖子是不现实的,我们把愿景分为了三个阶段目标:

  1. 跑通主流程测试,验证技术方案

    目的:对于不常变的功能,做到脚本自动化测试

  2. 搭建Mock系统,基于Mock进行测试

    特定的场景需要特定的数据,基于mock可以做接口容灾,功能一致性测试(固定的数据对应固定的展示)。

  3. 测试用例支持配置化

    主要目的:可通过配置,无代码完成测试用例执行

架构设计

image.png

除了最下边的依赖层外,整个方案包含三层:

一、底层服务层

包含路由跳转、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,这样就可以拦截,替换我们准备的数据。

文档参考:minitest.weixin.qq.com/#/minium/Py…

由于我们既想Mock,还行进行数据录制,所以并没有使用 mock_wx_method() 进行mock,而是使用 evaluate() 来进行代码注入。

文档参考:minitest.weixin.qq.com/#/minium/Py…

通过 evaluate() 可以向小程序 app Service 层注入JS代码并执行。

Mock的方式有两种:

  1. 通过替换URL,把接口地址替换为YAPI的Mock数据地址
  2. 通过替换返回数据,返回本地的 mock 数据

前文提到数据录制,主要目的是为了解决,当需要使用大量本地 Mock 数据的时候,全部靠人工仿造数据太过麻烦。
于是我们想到在脚本运行期间,访问测试或线上环境的接口,拦截并把数据保存在本地。
这样我们只需要手动修改部分数据,即可 Mock 整个测试流程的数据。

大体流程如下:

小程序脚本自动化测试方案-1.png

通过 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 还可以做接口容灾测试等测试。

因为每个页面的操作已经函数化,可以把 “流程测试”中的代码变成配置化,测试人员可以通过配置要走的页面、需要点击的模块、等待时长等完成测试用例的脚步配置。