其他测试框架 我们在 Glow 如何测试 React Native 项目

Miles · April 12, 2018 · Last by Miles replied at May 23, 2018 · 4586 hits

重新详细写了一些关于 React Native 自动化测试的想法, 发布于公司的tech blog - 上篇, 下篇

由于React Native可以带来高效方便的开发模式, 我们去年底开始做的一个新App选择使用RN来进行开发。 随着开发模式的逐渐成熟, 对RN项目的QA也在不断探索中慢慢完善, 最终选择了Detox和jest+enzyme来做各种方面的测试。 这篇文章将介绍一下我们是如何测试整个React Native项目的。

顺便插一句: We are hiring! 阅读过程中有志同道合或者感兴趣的小伙伴可以联系我, miles@glowing.com。 具体要求啥的可以参考在testerhome发的招聘贴: 这里

High-Level Thoughts

Automation and Test pyramid

自动化测试的重要性这里就不多赘述了, 用自动化来代替重复无聊的人工测试是必然要走的一条路。(要是我天天做重复测试的话下周就想换工作了-_-#)。至于怎么样合理的设计一套自动化测试的体系, 我参考了标题链接里的观点。 文中核心观点是一套优秀的自动化测试体系要平衡各种类型的自动化测试, 关键点在于要有大量的low-level unit test 和较少但覆盖重要流程的UI Automation测试。

Image

这样分布的原因是E2E Automation的开发和维护成本太高, 大量的E2E case会严重拖累QA人员对整个测试系统的维护效率。 相比来说大量的独立快速的Unit test和适量的集合性测试可以高效高速的测试App中的细节逻辑。 大约10/20/70的分布比例在我看来是一个比较好的平衡点。(其实在测试的世界里没有什么是真理的, 大家可以根据自己项目的实际情况来考虑怎么安排这个平衡点)。

简单逐条介绍下对每种类型Automation的想法:

E2E

E2E测试最大的优点是真正的从User Interface对App进行测试, 并且可以测试到完整全面的集成系统。并且可以代替一定量的手工测试工作。 因为E2E测试是模拟真实用户scenario的特性, 在任何测试体系中E2E测试都是不可缺少的。

但是E2E测试的缺点也很明显:

  • E2E测试依赖于测试Build和测试环境。 经常E2E case挂了是因为各种非bug的原因,需要花时间和精力去维护测试Build和环境才能保证E2E case都pass。
  • 要花很长时间才能找到真正的bug。 比如登录的时候一个严重的bug导致所有的case都fail了, 必须要等这个问题修复了才能继续测其他的flow, 效率就变低了。
  • 在fail的E2E case里找root cause很痛苦。 经常需要人工试好几次才知道为什么case会fail。
  • 小的bugs很难被发现。 E2E case的assertion经常忽略掉不会影响整个Flow的bug, 但这些bug是不可接受的。
  • E2E tests的不稳定性。 实践过的人都明白当你的E2E test挂了一些, 但花费半天去研究后发现只是case意外的fail了(比如network慢, 测试机慢, 意外的dialog 等等)。
  • 高维护成本。 当UI或者功能变化的时候, 维护E2E测试的成本是很高的,如果E2E带来的收益还比不上维护他们的成本, 就得不偿失了。

因此在我们的测试体系中, E2E测试会被控制在一个尽量小的量, 但会覆盖重要的功能。

Think smaller, not larger - Unit Tests

所谓Unit test,目的是保证你的codebase中的一小部分代码正确的工作。 它可以包含从一个method岛一整个class, 或者从一个用来计算的util到一个component的UI tree。 总之目的就是保证每一块代码都按照它被设计的目的工作。

Unit Test的优点:

  • 快速! 运行unit test和E2E相比是非常快的, 特别是mock了一些被测unit不关心的外部模块的时候, 比如network request, db request,etc.
  • 可靠。 Unit test比依赖其他的service
  • 可以把bug独立出来。 比如一个unit test挂了, 那只要找到这个unit就可以知道bug在哪里, 不需要花很多成本去narrow down bug。

Integration test

集成测试是指高于Unit Test一层的测试, 目的是保证每个unit组装起来也是work的。 比如RN中component作为一个unit, Redux作为一个unit, Integration就可以把他们作为一个完整的测试对象去测。 或者Api层, DB层的集成测试。 总之就是保证一个个小unit组装起来也是work的。

Integration可以让team对整体的测试质量更有信心, 具体的实现方式可以根据不同的项目来决定。

E2E Test vs. Unit Test

E2E和Unit Test的优缺点都很明显, 因此我们要聪明的去合理利用他们。永远不要忘记整个测试的目的是抓住bug并且修复他们。 所以我们理想的模式是这样的:

  • Unit test覆盖所有核心逻辑的细节(在RN中也包含UI React tree的测试)
  • E2E覆盖main flow
  • E2E test暴露出bug -> 用Unit test重现bug -> Unit Test保证这个bug不会再出现。
  • 如果一个bug能用更unit的测试去regression, 就不用UI层面的测试去覆盖
  • 减少重复冗余的测试case

Implementation in Practice

E2E - Detox

Detox是Wix公司的一款开源Automation框架,尝试了一下效果还不错, 介绍一下优点:

  • 支持Android和iOS, 我们的RN code在iOS和Android结构都相同, 因此可以写一套E2E case同时适配2个平台
  • 自动Syncronized执行脚本步骤。 Detox会自动监测app里的async操作,当app完全idle的时候再执行下一步(后面会详细解释)
  • 支持RN的TestID。 React Native的元素都可以加一个TestID的prop用来给Detox作selector。(类似传统UI测试框架给元素加id)。 因为我平时也会开发App,所以需要加TestID的地方就自己加了。
  • 支持各种Test runner。 根据喜好可以选择mocha, AVA, jest等。 我们为了统一, ut和E2E都选择的jest
  • setup环境,build测试版本以及运行测试比较简单。 比起传统的Appium, calabash等基本不需要很多成本就能把测试跑起来。
  • React Native community比如Microsoft, Callstask.io, Wix都有一些资源来维护开发这个框架, 社区也比较活跃。

着重介绍一下我选择它的最重要原因 - Automatically synchronized:

先举个例子 - Detox case vs. Calabash (之前我们选用的测试框架,语言是ruby):
比如我们要点击ButtonA, 进入第二个页面后点击ButtonB. 2个页面之间有一些animation和network request。
传统的Calabash case可能会这么写:

touch "* id:'ButtonA'"
wait_for_element_exists "* id: 'ButtonB'"
sleep 2
touch "* id: 'ButtonB'"

原因是因为animation的时候ButtonB的element其实已经存在了, 但其实是并不可点的。为了减少case不必要的fail, 就迫不得已的加了一些sleep语句。 如果sleep的时间少, 当测试运行的机器比较慢的时候就会fail, sleep多了自然case就慢了。

在detox的case写起来就比较直观了:

await element(by.id('ButtonA')).tap();
await element(by.id('ButtonB')).tap();

因为detox自动sync的特性, 当ButtonA被点击之后, App的animation, network request都运行完毕, app完全idle的时候,点击事件才会resolve。 因此ButtonB被点击的时候就不需要考虑ButtonB是不是可点击的状态了。 这就是所谓的Automatically Synchronized.

具体到Detox实现这个功能的方式, 它的底层在iOS和Android分别使用了Google的Earl Grey和Espresso。
两者的文档介绍如下,就不详细解释了:

Earl Grey:
1. automatically sync with the UI, network request and various queues, all manually implement customized timings.
2. UI in a steady state before actions are performed. Increase test stability and makes tests highly repeatable.
3. Earl runs in the same process as the app under test, so it has access to the same memory as the app, allows for better synchronize. such as ability to wait for network requests.

Espresso:
1. let you leave your waits , syncs, sleeps behind white it manipulates and asserts on the app UI when its at rest.
2. perform action or assertion until the following sync conditions are met:
1. message queue is empty
2. no instances of AsyncTask currently executing a task
3. All developer-defined idling resources are idle - Loading data , establishing connections with databases and callbacks, Managing services(system service or instance of IntentService), business logic such as bitmap transformations.

Detox用来判断app是否idle追踪的包含下面这些:

  • 网络请求
  • App里的动画
  • timers, 比如setTimeout
  • RN以及native里的async操作等
  • JS里的asnyc操作等

Unit Test & Integration Test - Jest and Enzyme

Jest snapshot test

Jest提供的snapshot是我认为很cool的一种测试方式, 他的工作方式如下(以react tree 测试为例):

Render一个UI component -> Take a snapshot -> 和之前的snapshot 比较 ->
snapshot比较的结果不同时, 如果是unexpected, 那要去看是不是有bug。 如果结果是expected, 就更新snapshot。

同时这种snapshot的方式不仅可以用来测试UI tree, 也可以用来测试任何可以serialize的东西。

比如我们有一个clearRedux的action来reset redux中除了app navigation stack的所有state。测试case本身就可以写的很简单:

expect(rootReducer(originState, clearReduxAction())).toMatchSnapshot();

在对应的snapshot文件中可能有一个很复杂的state object,但我们并不关心这个object是什么样的, 只关心snapshot的diff。 如果snapshot和之前的snapshot一样, 我们就认为reducer在接到clearRedux之后的行为是正确的。

Others

同时jest也提供了很方便的mock module/promise的方法和创建spy函数的方法, 想了解的小伙伴可以去他们github上看一下。由于很多别的test runner也有类似的特性就不详细展开了。

Jest 的ut在执行的时候是按照文件分别并行执行的, 200多个ut大概也就需要10到15秒的时间就能执行完毕, 同时也有很酷的coverage report, watch mode等好用的feature。

Enzyme

Enzyme是Airbnb开源的JS test utility for React。 它提供了直接操作component中prop和state的API。

在实际应用中我选择只使用他shallow render component的模式,shallow render的意思是component的子component并不会被完整的render出来。 这种方式很适合做分层Unit test测试。

举个简单的例子:

比如有个Example component的render函数是这样的:

...
render() {
return (
<Example>
<PrimaryButton
onPress={this.props.function1}
>
</Example>
)
}

我的case是这样的:

const mockFunc = jest.fn();
const wrapper = shallow(<Example function1={mockFunc}/>>)
wrapper.find('PrimaryButton').simualte('press');
expect(mockFunc).toBeCalled();

我只关心当前被测的Component的行为, 就是PrimaryButton被点击的时候prop.function1函数被call到了。至于PrimaryButton这个component里面是不是有bug, 这个callback是不是真的被调到我这个case并不关心(当然会有另一个ut专门去测试PrimaryButton的行为)

好处是PrimaryButton自身的bug只会导致一个ut fail, 其他所有用到这个Component的地方都不会受影响。

Enzyme也提供了Full Dom render的方法, 集成测试的时候也可以适量的选择这种方式, 类似于headless的automation。

Future

这套测试系统目前也还在开发过程中, 理想的目标点有这些:

  • 完整全面的unit tests - 给我们足够的信心去refactor code和做regression
  • E2E test 覆盖主要的flow
  • 一个稳定的持续集成系统来自动的运行这些测试

慢慢来吧😂 也希望有看过文章感兴趣的同学们把简历丢过来~~ miles@glowing.com!

共收到 8 条回复 时间 点赞
Miles #1 · April 12, 2018 作者

第一次写这样的文章, 欢迎讨论指正~

为了招聘,也是拼了

Miles #3 · April 12, 2018 作者
恒温 回复

哈哈哈 顺便整理整理自己的思路, 也是希望有志同道合的工作伙伴啊。

写的不错。单测一般由开发编写,而E2E和Integration test一般由测试编写,在不统一管理的情形下,由于思路的差别和工作的分开,有可能出现重复测试、优先级理解不一致等问题。整体测试策略很依赖测试人员的个人能力。

Miles #5 · April 26, 2018 作者
Goodboy 回复

谢谢~ 写的其实挺潦草的,准备整理一下分开几篇详细写一下。 我们目前从ut到e2e都是我在写。 感觉dev目的和qa不一样, 我想让这套完全代替手工的regression。
现在瓶颈在code写的逻辑层和现实层没有分开,我准备先refactor code。
其实qa dev本质都是engineer 作为qa有时间改代码写ut就都一起做了

顶一下。

  1. Jest的link贴错了哦。
  2. 想问一下RN有自带的component test infra或者能复用react的吗?我印象中像react/angular都自带了component test(UT)。
Miles #7 · April 28, 2018 作者
andward_xu 回复

和react没啥区别 因为ut测试还不涉及交给native renderer做的事情。 rn会提供component的 auto mock, 写测试的时候不大用关心这个(所以我也没特别研究过)

Miles #8 · May 23, 2018 作者

重新详细写了一些关于 React Native 自动化测试的想法, 发布于公司的tech blog - 上篇, 下篇

欢迎来一起讨论。

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up