重新详细写了一些关于 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 测试会被控制在一个尽量小的量, 但会覆盖重要的功能。

Think smaller, not larger - Unit Tests

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

Unit Test 的优点:

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 并且修复他们。 所以我们理想的模式是这样的:

Implementation in Practice

E2E - Detox

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

着重介绍一下我选择它的最重要原因 - 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 it’s 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 追踪的包含下面这些:

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

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

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


↙↙↙阅读原文可查看相关链接,并与作者交流