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

Miles · 2018年04月12日 · 最后由 Miles 回复于 2018年05月23日 · 509 次阅读

重新详细写了一些关于 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 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 追踪的包含下面这些:

  • 网络请求
  • 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 条回复 时间 点赞

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

为了招聘,也是拼了

恒温 回复

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

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

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)。
andward_xu 回复

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

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

欢迎来一起讨论。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册