Qtest测试之道 广告 sdk 自动化测试实践
背景
广告 sdk 是什么
- 广告 sdk,是为接入移动广告平台的开发者提供的应用程序接口 api 的程序集合。广告 sdk 支持如下功能:
- 广告请求
- 广告打点
- 广告渲染
- 第三方 app 可以嵌入广告 sdk,并且调用 sdk 的接口完成广告请求、广告展示、广告渲染、广告打点等逻辑。应用开发者可以关注自己的应用开发,而与广告相关的逻辑均交给广告 sdk 进行处理。
功能测试背景
- 近半年来,广告 sdk 提测频繁,版本迭代快,导致测试排期紧张,功能测试同事压力大。
- 广告 sdk 接入了多家广告平台,并且每一家广告平台都拥有十多种不同格式的协议,使得广告打点数据校验逻辑异常复杂。通过人工检查难以避免校验疏漏。
- 大量的广告打点数据校验导致回归测试工作量大。
自动化测试目的
- 通过自动化测试方案,提升测试效率,解放人力
- 通过代码的严格检查保证打点数据的正确性
- 形成 sdk 自动化测试的解决方案,为其他 sdk 业务测试团队提供借鉴思路
基本的测试场景
下图展示了 sdk 功能测试的基本场景:
最常见的测试场景步骤:
- 使用嵌入了 sdk 的测试 demo,请求一次广告。sdk 是 jar 包形式,sdk 运行需要 Android 上下文支持,所以需要嵌入测试 demo 进行测试。
- 使用 fiddler 抓取 sdk 广告请求,并且校验广告请求数据是否正确。广告请求数据异常会导致线上无法返回广告。例如,某个版本在测试环境中因为某个字段的类型不正确,导致测试环境中无法返回广告。
- 依据测试 case 在 fiddler 中 mock 广告数据返回给 sdk。为什么需要 mock 服务端的广告数据呢?因为广告 sdk 针对不同类型的广告数据,会有不同的处理逻辑。例如,对于下载类广告和原生广告,同样是点击操作,而触发的广告行为完全不一致。所以功能测试过程中需要 mock 大量的不同类型的广告数据,来覆盖到 sdk 不同的功能逻辑,保障 sdk 的质量。
- 展示 sdk 获取到的广告。对广告进行展示,覆盖用户场景。
- 校验广告展示打点。打点数据涉及计费结算,数据正确性需要着重保障。
- 依次进行重复曝光、点击广告、下载等场景测试。
测试难点
虽然测试场景和测试步骤看似简单,但是功能测试依然面临如下几个难点:
-
打点数据多样性
- 广告 sdk 接入了多家广告平台,每个广告平台有 10 多种的广告协议。广告协议中携带的 json 数据中包含了 30-40 个数据字段。功能测试同事需要掌握大量的协议格式,才能保证打点数据的正确性。多样性的广告协议,给功能测试同事增加了非常大的工作量。自动化测试也同样面临打点数据类别区分以及正确性校验的难题。
-
打点时序控制
- 从前面的测试场景,我们知道各种类型的打点数据触发时机不一样,并且有严格的先后顺序。功能测试过程中,为了覆盖到所有的打点数据校验,测试同学需要设计各种操作场景来触发 sdk 打点行为。广告数据打点是在移动端进行触发,而抓包和 mock 在 pc 端进行。如何对两者进行协同,对打点时序的进行严格控制,也同样是自动化测试的难点。
-
mock 数据频繁
- 服务端返回的广告数据直接影响 sdk 广告行为和打点数据。为了提高测试覆盖度,测试过程中往往需要构造各种类型的广告数据,达到覆盖各种测试场景的目的。自动化测试过程中,如何将设计的测试场景和 mock 数据进行关联,并且进行动态的变换同样是难点之所在。
自动化测试方案需要满足的需求
依据基本的测试场景和功能测试难点,广告 sdk 自动化测试需要满足如下需求:
- 自动触发广告行为
- 包括广告请求、曝光、点击等行为,还包括清理缓存,杀进程等行为
- 只有自动触发广告行为,才能让自动化测试真正脱离人工干预
- 动态 mock
- 需要依据设计的自动化 case,动态的 mock 不同的广告数据,覆盖各种测试场景和 sdk 的广告处理逻辑
- 数据校验
- 自动化测试最重要的一个环节,需要依据期望结果,校验实际结果,从而判定该条 case 是否通过。从而发现 sdk 存在的缺陷。
方案框架示意图
整个自动化测试框架,主要分为测试 demo、mock 服务器和控制服务器三部分:
- 测试 demo(sdk 宿主 app)
- 测试 demo 嵌入了广告 sdk,是测试的主体程序。
- mock 服务器
- 同时也是代理服务器
- 抓取广告请求数据、打点数据
- 为 sdk mock 广告数据
- 控制服务器
- case 管理
- 控制 case 执行
- 协同 sdk 与 mock 服务器进行场景测试
- 进行结果校验
分层设计
对整体架构进行了分层设计,主要分为框架层,逻辑层和数据层。
- 框架层,主要采用开源或是第三方框架作为底层计算、调度支持。
- 逻辑层,主要依据底层框架开放的接口,实现符合自身业务需求的代码逻辑。例如,控制服务器通过逻辑层对具体的测试场景进行实现。
- 数据层,主要是测试过程需要用到的一些数据,包括测试 case、mock 的广告数据等。
通过对架构进行分层设计,将框架、逻辑和数据做分离,使得架构的健壮性和可复用性更强。自动化测试面临的一个问题是,方案的可复用性是否足以应对业务逻辑的变更。在我们的业务中,即使服务端与 sdk 之间的交互协议发生了变化,只需要对数据层的数据做变更即可。而不需要对逻辑和架构做修改。
控制服务器
控制服务器是整个自动化测试框架的大脑,控制 case 执行,协同测试 demo 和 mock 服务器工作,并且对测试结果数据进行校验。说到自动化测试,大家可能有疑问,如何书写测试 case 并对 case 进行管理,如何控制 case 执行?如何控制测试 demo 自动完成广告请求、曝光、点击等操作?答案就是两个测试框架:cucumber和appium。
cucumber
Cucumber 是一个能够理解用自然语言描述的测试用例的支持行为驱动开发(BDD)的自动化测试工具,用 Ruby 编写,支持 Java 和·Net 等多种开发语言。 ---百度百科
cucumber 有两个特性:
- 理解自然语言描述的测试用例
- 支持行为驱动开发(BDD)
下面详细了解一下控制服务器的数据层和逻辑层如何进行实现。
- case 描述(数据层)
我们将测试场景,抽象成一个个测试的执行步骤,然后用自然语言描述成 cucumber 支持的格式。最后 cucumber 通过 BDD 框架控制 case 执行。
自动化测试 case 示例:
场景:MAX原生广告"请求"及"请求打点"完整性校验
假设 caseID"10001"
假设 初始化mock文件为"360原生广告"
假设 打开原生广告界面
并且 填写广告条数"3"
并且 填写广告位条数"1"
当 点击按钮"获取广告"
那么 请求的完整性如"juhe-adrequest-schema"描述
那么 请求及字段值校验如下
|count| url | field | value |
| 1 |https://domain?pver=2| adtype | 3 |
| 1 |https://domain?pver=2| adspaceid | 5a56tq |
那么 请求的完整性如"juhe-tk-request-schema"描述
并且 聚合请求打点字段校验
|count |type | field | value |
| 1 | 0 |agspaceid | ag56q0xf |
| 1 | 0 |adspaceid | 5a5q08xf |
| 1 | 0 |agappkey | ag318422 |
| 1 | 0 |adappkey | 318422 |
| 1 | 0 |plid | 1 |
| 1 | 0 |channelid | 7 |
| 1 | 0 |status | 0 |
| 1 | 0 |reqnum | 3 |
| 1 | 0 |resnum | 1 |
上述的自动化测试 case 描述了对广告请求字段进行校验的测试场景。测试 case 采用自然语言对测试场景进行描述,包含测试过程中前置条件、测试步骤和测试结果校验等信息。通过 cucumber 支持的假设、当、并且、那么等关键字对测试 case 进行定义。通过该种方式整理 case,测试场景和校验逻辑一目了然。
- case 执行(逻辑层)
虽然 cucumber 支持的 BDD 框架控制 case 执行,但是需要我们自己对测试 case 描述的每个步骤逻辑进行实现。这些步骤中,需要实现 case 描述的具体逻辑。
例如,case 中描述的步骤 “点击按钮”,步骤实现逻辑需要调用 appium 相关接口对测试 demo 按钮进行点击。
@假设("^caseID\"([^\"]*)\"$")
public void getCaseID(String arg0) throws Throwable {
// Write code here that turns the phrase above intoconcrete actions
// throw new cucumber.api.PendingException();
}
@假设("^打开原生广告界面$")
public void openNativeActivity() throws IOException {
}
@并且("^填写广告条数\"([^\"]*)\"$")
public void fillInAdNum(String adnum) throws IOException {
}
@当("^点击按钮\"([^\"]*)\"$")
public void clickButton(String btnType){
}
@那么("^请求及字段值校验如下$")
public void checkUrlField(List<UrlRequestField> list){
}
demo 自动化方案
方案选型
通过 demo 对广告 sdk 进行自动操作,是整个自动化测试最为关键的一个环节。只有测试 demo 可以 “自主” 请求广告、曝光、点击才能完全进行自动化测试。如果 sdk 不能自主触发广告行为,会直接导致测试 case 执行失败。在考虑 demo 自动化测试方案时,面临两种选择:接口调用方式以及 UI 自动化。
接口调用
广告 sdk 的广告行为,都是通过宿主 app 调用 sdk 的各种 api 进行实现的。例如,宿主 app 的广告请求行为,也就是调用 sdk 的广告请求 api 完成。所以 demo 自动化方案,也可以通过 api 调用方式进行。该方案有如下特点:
- 直接调用 sdk 接口
- 实现简单
- 传递参数复杂(需要特定的语义描述复杂的测试场景,各种接口调用顺序,以及各个接口的参数)
- 不支持清空缓存、下载第三方 app 等操作(sdk 需要对一些复杂的操作场景进行测试)
UI 自动化
UI 自动化通过操作测试 demo 完成对 sdk 的广告行为,该方案相当于模拟用户的操作行为,最接近真实的测试场景。该方案有如下特点:
- 需要第三方 UI 自动化框架支持
- 模拟用户操作,符合实际操作场景
- 支持复杂的操作场景(杀进程、清空缓存等)
- 需要进行机型适配(权限弹窗点击、安装弹窗等)
考虑到 sdk 测试,很多时候需要对测试 demo 进行杀进程、清理缓存、安装第三方 app 等操作,故最终选择 UI 自动化测试实现 demo 自动化方案。
UI 框架选型
前面提到了,我们为了覆盖复杂的测试场景选择 UI 自动化方案对广告 sdk 进行自动化操作。考虑到广告 sdk 有 Android 和 ios 两个版本,需要 ui 自动化框架具有跨平台特性。我们采用 appium 测试框架实现测试 demo 的自动化操作。
Appium 是一个开源的、跨平台的自动化测试工具。它适用于原生应用、混合应用和移动网页应用。该框架支持 iOS,Android 和 FirefoxOS 的模拟器和真机。
我们通过 appium 对 demo 进行自动化操作,从而完成各种测试场景的操作行为。
UI 自动化方案要点
UI 自动化方案的健壮性直接关系到自动化测试是否能够顺利完成。对测试 demo 进行自动化操作的过程中,如果出现获取控件元素失败等异常情况,则会导致该条 case 执行失败。UI 自动化方案实现,面临如下问题:
- 权限弹窗问题
- 广告 sdk 有一些测试场景,需要清理缓存,然后重新启动。这个时候在某些品牌手机上会有权限弹窗提示,影响测试 demo 按钮点击,从而影响 case 顺利执行。
- 安装弹窗问题
- 广告 sdk 的下载类广告,需要下载第三方 app 并且安装。安装过程中会有安装界面调起,这时候覆盖测试 demo 影响测试正常进行。
考虑解决方案如下:
- 轮询(等待)+ 重试机制
- 针对第三方 app 下载时间不确定性,可能导致安装界面覆盖测试 demo。我们考虑的方案是,等待 app 下载完成,并且安装完成再进行 demo 上的下一步操作。
- 对某些控件的点击增加重试机制,保障测试场景顺利进行。
- 需要对轮询机制进行条件限制(轮询次数或者轮询时间进行判断)避免陷入死循环。
- 兼容性考虑
- 考虑多条件组合查询,利用 xpath、resourceid 或 text 对控件元素进行定位。
- 权限弹窗上的按钮在不同的手机上可能有不同的 resource-id,这个时候可能需要借助 xpath 方式进行元素定位,甚至需要 text 进行辅助。
- 执行步骤之间的停顿
- 由于不同手机的性能不一样,处理 ui 操作的速度也不一样。很多时候 cucumber 控制步骤之间,需要一些停顿时间才能保证下一步骤执行前,上一个步骤已经执行完毕。
- rerun 机制
- 即使对兼容性方案进行了充分考虑,还是存在意外情况导致 ui 操作执行失败,导致该条测试 case 测试不通过。此时,通过 rerun 机制排除意外因素对测试结果干扰。
- 效率和准确性权衡
- 为了让 ui 自动化方案可以适配更多手机,可能增加更多的判断逻辑,这样兼容性会更强,但同时也会影响代码执行效率。
- 元素定位不同的定位方式也有区别:xpath 定位控件效率低,但兼容性更强,定位元素准确。而 resource-id 定位控件快速,但是兼容性较弱。
- 在实际项目中,我们需要依据实际项目情况对兼容性和效率进行权衡。如果需要适配大量的不同机型的手机,那么需要对兼容性考虑更全面一些。
控制服务器框架图
要点:
- cucumber 是广告 sdk 自动化测试的控制框架。
- cucumber 通过 mock 配置文件告知 mock 服务器(anyproxy),需要抓取什么样的数据包,以及需要如何 mock 数据。
- cucumber 通过 appium 框架提供的 api,完成对 demo 的自动操作。
- cucumber 需要从数据库中获取结果数据,并对结果数据进行校验,得出测试结论。
mock 服务器方案
anyproxy
mock 服务器主要采用阿里开源的代理服务器 anyproxy 进行实现。anyproxy 主要有如下特点:
- 基于 nodejs
- 提供 web 版界面,可以实时观测网络请求
- 开放接口,允许用户自定义规则,易于二次开发
数据层
前面提到控制服务器 cucumber 需要给 mock 服务器下发 mock 文件,对 mock 服务器的 mock 逻辑进行控制。主要涉及以下两个文件:
-
mock 配置文件
- json 形式,以 key-value 形式指定 mock 规则。json 中 key 字段为需要抓包或 mock 的 url,value 是相应的 mock 数据文件地址。
- 指定抓包的 url:由于在测试过程中,代理服务器捕获的网络请求不只有 sdk 发送的请求,也有 pc 端和手机端其他的数据流量。为了避免这些数据影响测试结果的校验,所以在 mock 服务器端指定需要捕获和 mock 的网络请求。
-
mock 的数据文件
- 不同的测试场景,需要 mock 不同的广告数据,覆盖 sdk 不同的代码逻辑。针对每条测试 case,都需要指定该测试场景对应的 mock 数据。
逻辑层
虽然数据层规定了 mock 规则以及给定了 mock 数据。但是还是需要在逻辑层对具体的 mock 逻辑进行实现。而 anyproxy 提供一系列接口供我们方便实现抓包和 mock 逻辑。mock 服务器在每一个 case 执行结束,都需要将抓取的广告打点数据存入数据库,供控制服务器进行结果校验。
mock 服务器数据交互图,如下:
动态 mock 实现
前面已经提到自动化测试方案需要满足动态 mock 的测试需求。我们通过mock 配置文件 + 控制信号实现了动态 mock。
mock 服务器的 mock 规则是通过 mock 配置文件进行控制的。mock 配置文件指定了 mock 服务器需要抓取的数据包和对应的 mock 数据。而通过控制信号,控制服务器会依据测试场景动态更改 mock 配置文件。通过控制信号和 mock 配置文件的协同,实现了动态 mock。
数据流图
为了保证每条测试 case 顺利执行,控制服务器通过控制协议协同测试 demo、mock 服务器一起工作。
控制协议数据流图如下所示:
包括三种控制协议:
每种控制信号都携带 caseID,将 sdk 打点数据与测试场景进行关联。
- 测试开始信号:
- 控制服务器通知测试 demo 和 mock 服务器新的一条测试 case 开始执行。
- mock 服务器接收到测试开始信号,需要重新从服务器获取 mock 配置文件和 mock 数据文件。接下来依据 mock 配置文件的规则进行抓包和 mock。从而满足该条测试 case 的测试需求。
- 测试结束信号
- 控制服务器通知测试 demo 和 mock 服务器该条 case 执行结束。
- mock 服务器接收该信号后,将抓到的数据包写入控制服务器数据库。
- 控制服务器从数据库读取测试数据,并对测试数据进行校验,输出测试结果。
- mock 文件更改信号
- mock 服务器接收到该信号后,重新从控制服务器端获取 mock 配置文件,更改 mock 规则。
下面通过 “原生广告请求打点校验” 测试 case 为例,说明动态 mock 如何生效:
- 控制服务器 cucumber 获取一条新的 case,准备该条 case 对应的 mock 配置文件和 mock 数据文件。
- 控制服务器依据 caseID,在数据库中创建新的数据项,用于存储 sdk 打点数据。
- cucumber 通过 appium 控制测试 demo 发送测试开始信号(控制信号)。
- mock 服务器收到测试开始信号后,从控制服务器获取该测试场景的 mock 配置文件和 mock 数据文件。
- 然后 cucumber 通过 appium 控制 sdk 执行一系列广告操作。
- mock 服务器依据 mock 配置文件的规则进行抓包和 mock。
- 控制服务器通过 appium 控制测试 demo 发送测试结束信号,mock 服务器收到该信号后,将捕获的打点数据写入控制服务器数据库。
- 在复杂的测试场景中,如果需要对 mock 规则和 mock 数据进行更换,那么控制服务器通过mock 更改信号通知 mock 服务器,重新获取 mock 相关文件。
- 最后控制服务器对数据库中的打点数据进行结果校验。
结果校验
自动化测试的最后需要对测试数据进行校验,得出测试结论。前面已经提到,mock 服务器会将捕获的 sdk 请求和打点数据,并且写入数据库。为了达到校验准确性,需要进行如下三个方面校验:
- 数据格式(json 字段类型、字段是否有缺失等)
- 请求数量
- 关键字段值
在我们的方案中,主要采用 json-schema 协议对网络请求的 json 数据进行校验。json-schema 是一种基于 json 格式定义的 json 数据结构规范。围绕 json-schema 协议有丰富的开源实现。通过这些基于 json-schema 协议的开源实现可以实现 json-schema 自动生成,json 数据验证。
下面看一下具体的 json-schema 示例:
原始 json
{
"id": 2,
"name": "An ice sculpture",
"price": 12.5
},
{
"id": 3,
"name": "A blue mouse",
"price": 25.5
}
生成的 json-schema
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Product",
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"description": "The unique identifier for a product",
"type": "integer"
},
"name": {
"description": "Name of the product",
"type": "string"
},
"price": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
}
},
"required": [
"id",
"name",
"price"
]
}
}
通过 json-schema 协议提供的语义,我们可以定义每个 json 字段类型,以及该字段的必要性等特性。与此同时,还可以对字段值进行一定的限定。int 类型的数据,可以指定范围,也可以依据业务场景限定为特殊的值。string 类型的值支持正则匹配。并且我们通过业务 json 模板文件可以自动生成校验数据需要的 json-schema。不需要我们进行手动构建,节省了大量的工作量。
方案运行情况
目前已经梳理了 75 条自动化测试 case,占到 case 总量的 25%,预计将来可以达到 40%。
目前自动化测试方案,已经部署到持续集成环境中,开发提测以及上线前都可以直接运行自动化测试方案对 sdk 进行自动化测试。自动化测试使得回归测试耗时从 10 小时/人次减少到 0.5 小时/人次。自动化测试方案提升了测试效率,解放了人力。并且通过严格的打点数据校验,避免了人工疏漏造成线上问题。