自动化工具 React Native 代码覆盖率获取探索 (一)

陈恒捷 for PPmoney · April 09, 2017 · Last by TD replied at August 30, 2023 · 5112 hits
本帖已被设为精华帖!

背景

最近开始做覆盖率的落地,刚完成 android jacoco 的,结果就遇上了项目架构调整,开始往 react native 方向走。而且按照新架构,主要逻辑代码都是在 react native 上编写,所以探究一下怎么获取 react native 的 js 代码覆盖率。

核心思路:

  1. 先通过单测获取覆盖率,了解 js 领域的覆盖率收集有什么工具可用
  2. 在覆盖率收集工具上找方法,找到获取覆盖率数据和生成报告的接口,为手工测试覆盖率报告生成做铺垫
  3. 找个 app 加入获取覆盖率数据、生成报告相关的接口调用,把整个流程(打开 app 后,在 app 置于后台或者定时发送覆盖率数据到服务器,服务器再自动生成报告)跑通。

搭建开发环境

不得不说,react native 的开发环境搭建文档写得非常好,国内也有一个 React Native 中文网 ,把这部分文档完全翻译过来,并且接地气地写上了国内的一些镜像地址。

搭建开发环境

具体大家可以直接看上面链接里的流程,这里就不再详述了。最终完成了 iOS 和 android 开发环境的搭建,并用 react-native run-iosreact-native run-android 两个命令验证成功。

下载并初始化示例项目

默认的 AwesomeProject 基本没有逻辑,做覆盖率尝试不是很够。找了下,找到了 facebook 的 2016 年 f8 app 。直接使用它就好了。

下载及初始化方法(简要版,详细的建议参照 github 上的文档):

  • 服务端 (官方教程有坑,此处的命令已经填坑了)
git clone https://github.com/fbsamples/f8app.git
cd f8app && npm install

# ios dependencies
cd ios; pod install; cd ..

# Import sample data(官方的说明,实际上运行百分百失败。详细原因请看末尾的踩坑记录。这里后续命令使用能成功运行的命令)
# npm run import-data # 这条命令运行会报错,请使用下面的命令
# download db 
wget https://raw.githubusercontent.com/ReactWindows/f8app/data/mongodb/db.zip

# unzip db
unzip db.zip -d f8_db

# run mongodb base on db backup above
mongod --storageEngine wiredTiger --dbpath f8_db/db

# Make sure mongodb is running. If it's running, one line starts with "mongodb" should exist
lsof -iTCP:27017 -sTCP:LISTEN
# If it's running, it should looks like below:
# COMMAND   PID        USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
# mongod  99400 hengjiechen    8u  IPv4 0x5fba934ae787aa99      0t0  TCP *:27017 (LISTEN)

# Start Parse/GraphQL servers
npm start

通过查看 http://localhost:8080/dashboardhttp://localhost:8080/graphql 确保服务开启成功,并且数据库有相应数据。

(主要看后面蓝色底色的那张图,确认有数据)

  • 客户端(iOS & Android)
# Android
react-native run-android
adb reverse tcp:8081 tcp:8081   # required to ensure the Android app can
adb reverse tcp:8080 tcp:8080   # access the Packager and GraphQL server

# iOS
react-native run-ios

运行效果:

收集单测覆盖率

根据 Unit Testing 可以看到,收集覆盖率的组件为 istanbul ,查了下,基本 js 覆盖率收集用的都是这个工具。

另外,由于 f8 项目是官方写得,Facebook 不使用上面 Unit Testing 里面使用的 Mocha 框架,而是用 Facebook 自己的 jest。经过查阅,jest 本身带有覆盖率相关的配置项,集成了使用 istanbul 收集覆盖率的功能,而且 f8 里面也有一些 jest 的测试用例,直接拿来尝鲜下。

单测相关文件在 ./js/reducers/__tests__ 文件夹里面有一些,通过 npm test 命令即可自动执行,结果类似下面:

$ npm test

> F8v2@0.0.1 test /Users/hengjiechen/Develop/ReactNative/f8app
> jest

Using Jest CLI v13.0.0, jasmine2, babel-jest
 PASS  js/reducers/__tests__/maps-test.js (0.358s)
 PASS  js/reducers/__tests__/schedule-test.js (0.376s)
 PASS  js/reducers/__tests__/notifications-test.js (0.381s)
 PASS  js/tabs/schedule/__tests__/formatDuration-test.js (0.108s)
 PASS  js/tabs/schedule/__tests__/formatTime-test.js (0.099s)
12 tests passed (12 total in 5 test suites, run time 4.813s)

然后根据官方的 配置文档,加入覆盖率相关配置。需要修改 ./package.json 文件,具体修改如下:

diff --git a/package.json b/package.json
index 17d9914..27d060e 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,9 @@
       "providesModuleNodeModules": [
         "react-native"
       ]
-    }
+    },
+    "collectCoverage": true,
+    "coverageDirectory": "coverage"
   },
   "engines": {
     "node": ">=5.0",

PS:也可以把上面的代码块内容保存到 coverage.patch 文件,存到 f8app 根目录,然后 git apply coverage.patch 直接应用变更。

加入覆盖率配置后,同样 npm test ,输出如下:

$ npm test
> F8v2@0.0.1 test /Users/hengjiechen/Develop/ReactNative/f8app
> jest

Using Jest CLI v13.0.0, jasmine2, babel-jest
 PASS  js/reducers/__tests__/schedule-test.js (0.166s)
 PASS  js/reducers/__tests__/maps-test.js (0.166s)
 PASS  js/reducers/__tests__/notifications-test.js (0.21s)
 PASS  js/tabs/schedule/__tests__/formatTime-test.js (0.079s)
 PASS  js/tabs/schedule/__tests__/formatDuration-test.js (0.087s)
12 tests passed (12 total in 5 test suites, run time 3.347s)
------------------------|----------|----------|----------|----------|----------------|
File                    |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
------------------------|----------|----------|----------|----------|----------------|
 reducers/              |    70.37 |    71.43 |    73.33 |    81.82 |                |
  createParseReducer.js |      100 |      100 |      100 |      100 |                |
  maps.js               |      100 |      100 |      100 |      100 |                |
  notifications.js      |    59.09 |       56 |     62.5 |    65.52 |... 107,108,110 |
  schedule.js           |    77.78 |    78.57 |       75 |      100 |                |
 tabs/schedule/         |      100 |    85.71 |      100 |      100 |                |
  formatDuration.js     |      100 |      100 |      100 |      100 |                |
  formatTime.js         |      100 |    66.67 |      100 |      100 |                |
------------------------|----------|----------|----------|----------|----------------|
All files               |    78.38 |     74.6 |    77.78 |    88.24 |                |
------------------------|----------|----------|----------|----------|----------------|

html 的覆盖率报告放在 ./coverage/lcov-report/index.html 中。同时也有 json 格式、xml 格式的覆盖率数据。

小结

这次探索基本了解了 rn 项目的大概结构,以及单测中 js 覆盖率怎么获取。基本确定了采用类似 middleware 嵌入到应用中的方式应该可以用来做手工覆盖率收集。具体操作方案后续再继续探索。

意外惊喜

在了解 istanbul 的时候,找到了一个好玩的项目:istanbul-middleware 。在项目根目录执行一些命令,即可得到可以实时显示覆盖率的小 Demo 。

使用方法如下命令:

git clone https://github.com/gotwarlost/istanbul-middleware.git

# 启动带有实时覆盖率报告的网站
cd istanbul-middleware/test/app
npm install && node index.js --coverage

打开 http://localhost:8888 可以访问这个小网站,打开 http://localhost:8888/coverage/ 可以访问实时更新的覆盖率报告(实时更新是指操作后 F5 更新覆盖率报告即可得到最新的覆盖率情况,也可以自己二次开发加个轮询实现真正的实时显示)

大家可以探索下,看通过哪些用例可以让行覆盖率和分支覆盖率达到 100% : ) 。后面甚至可以在这个 Demo 基础上进行小网站的功能扩展,甚至拿个单页应用来替代这个网站,开展一下覆盖率小竞赛,让大家更有乐趣地去了解和应用代码覆盖率这个工具。

踩坑记录

运行 npm run import-data 提示错误

错误信息:

> F8v2@0.0.1 import-data /Users/hengjiechen/Develop/ReactNative/f8app
> babel-node ./scripts/import-data-from-parse.js

Loading Speakers
SyntaxError: Unexpected token P in JSON at position 0
    at Object.parse (native)
    at /Users/hengjiechen/Develop/ReactNative/f8app/node_modules/node-fetch/lib/body.js:43:15
    at process._tickDomainCallback (internal/process/next_tick.js:129:7)

原因: 导入数据依赖的一个远程服务器 Parse.com 。这个服务器已经关站了,导致导入数据时服务端会返回 Parse.com has shutdown - https://parseplatform.github.io/,json 解析器解析出错。

解决方法:改用 github 上其它同学备份的 mongodb 数据库。

# download db 
wget https://raw.githubusercontent.com/ReactWindows/f8app/data/mongodb/db.zip

# unzip db
unzip db.zip -d f8_db

# run mongodb base on db backup above
mongod --dbpath f8_db

参考资料:https://github.com/fbsamples/f8app/issues/149, https://github.com/fbsamples/f8app/issues/156

使用 react-native run-ios 启动 iOS 客户端时,编译失败

错误信息:

An application bundle was not found at the provided path.
Provide a valid path to the desired application bundle.
Print: Entry, ":CFBundleIdentifier", Does Not Exist

原因:实际上出错的不是这个地方,是代码里有一个地方本身就会编译出错。react-native cli 的错误信息不大正确。

解决方法:参考下面的内容修改 f8app/node_modules/react_native/React/Views/RCTScrollView.m

...
@implementation RCTCustomScrollView
{
  __weak UIView *_dockedHeaderView;
}

// 增加下面这一行
@synthesize refreshControl = _refreshControl;

- (instancetype)initWithFrame:(CGRect)frame
{
  if ((self = [super initWithFrame:frame])) {
    [self.panGestureRecognizer addTarget:self action:@selector(handleCustomPan:)];
  }
  return self;
}
...

参考资料:https://github.com/fbsamples/f8app/issues/137

使用启动 ios 客户端后,terminal 报错

错误信息:

Failed to build DependencyGraph: Watchman error: Cannot read property 'root' of null. Make sure watchman is running for this project. See https://facebook.github.io/watchman/docs/troubleshooting.html.

原因:watchman 有问题,重装即可解决。

解决方法:brew uninstall watchman && brew install watchman -HEAD

参考资料:https://github.com/facebook/react-native/issues/1875

使用 react-native run-android 启动时报错

错误信息:

Execution failed for task ':app:processDebugManifest'.
> Manifest merger failed : Attribute activity#com.facebook.FacebookActivity@theme value=(@android:style/Theme.Translucent.NoTitleBar) from AndroidManifest.xml:55:11-70

错误原因:android mainfest 文件内容有问题,需要修改

解决方法:删掉 android/app/src/main/AndroidManifest.xml 文件里面的这一行(应该是第 55 行):

android:theme="@android:style/Theme.Translucent.NoTitleBar"

参考资料:https://github.com/fbsamples/f8app/issues/134

运行 android 客户端时,关闭首次打开的登录界面后直接 crash

logcat 错误日志:

E/AndroidRuntime( 1958): FATAL EXCEPTION: IntentService[RNPushNotification]
E/AndroidRuntime( 1958): Process: com.facebook.f8, PID: 1958
E/AndroidRuntime( 1958): java.lang.IllegalAccessError: Method 'void android.support.v4.content.ContextCompat.<init>()' is inaccessible to class 'com.google.android.gms.iid.zzd' (declaration of 'com.google.android.gms.iid.zzd' appears in /data/app/com.facebook.f8-1/base.apk)
E/AndroidRuntime( 1958):    at com.google.android.gms.iid.zzd.zzdL(Unknown Source)
E/AndroidRuntime( 1958):    at com.google.android.gms.iid.zzd.<init>(Unknown Source)
E/AndroidRuntime( 1958):    at com.google.android.gms.iid.zzd.<init>(Unknown Source)
E/AndroidRuntime( 1958):    at com.google.android.gms.iid.InstanceID.zza(Unknown Source)
E/AndroidRuntime( 1958):    at com.google.android.gms.iid.InstanceID.getInstance(Unknown Source)
E/AndroidRuntime( 1958):    at com.dieam.reactnativepushnotification.modules.RNPushNotificationRegistrationService.onHandleIntent(RNPushNotificationRegistrationService.java:20)
E/AndroidRuntime( 1958):    at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:65)
E/AndroidRuntime( 1958):    at android.os.Handler.dispatchMessage(Handler.java:102)
E/AndroidRuntime( 1958):    at android.os.Looper.loop(Looper.java:135)
E/AndroidRuntime( 1958):    at android.os.HandlerThread.run(HandlerThread.java:61)
W/ActivityManager(  745):   Force finishing activity com.facebook.f8/.MainActivity

错误原因:PushNotificationController 部分有问题,需要注释掉相关功能。

解决方案:参考以下 diff 内容修改 js 文件夹里的两个文件。

diff --git a/js/F8App.js b/js/F8App.js
index 9443433..ff45131 100644
--- a/js/F8App.js
+++ b/js/F8App.js
@@ -28,7 +28,7 @@
 var React = require('React');
 var AppState = require('AppState');
 var LoginScreen = require('./login/LoginScreen');
-var PushNotificationsController = require('./PushNotificationsController');
+//var PushNotificationsController = require('./PushNotificationsController');
 var StyleSheet = require('StyleSheet');
 var F8Navigator = require('F8Navigator');
 var CodePush = require('react-native-code-push');
@@ -77,9 +77,9 @@ var F8App = React.createClass({
   },

   render: function() {
-    if (!this.props.isLoggedIn) {
-      return <LoginScreen />;
-    }
+    //if (!this.props.isLoggedIn) {
+    //  return <LoginScreen />;
+    //}
     return (
       <View style={styles.container}>
         <StatusBar
@@ -88,7 +88,6 @@ var F8App = React.createClass({
           barStyle="light-content"
          />
         <F8Navigator />
-        <PushNotificationsController />
       </View>
     );
   },
diff --git a/js/setup.js b/js/setup.js
index b8134ab..0a0f962 100644
--- a/js/setup.js
+++ b/js/setup.js
@@ -63,9 +63,9 @@ function setup(): ReactClass<{}> {
       };
     }
     render() {
-      if (this.state.isLoading) {
-        return null;
-      }
+      //if (this.state.isLoading) {
+      //  return null;
+      //}
       return (
         <Provider store={this.state.store}>
           <F8App />

也可以直接把上面的内容保存到 fix_android.patch 文件,放到 f8app 文件夹。然后在 f8app 文件夹运行 git apply fix_android.patch 执行变更。

参考资料:https://github.com/fbsamples/f8app/issues/133

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 27 条回复 时间 点赞

最近也准备接入覆盖率平台,发现我们的应用也开始采用类似 rn 的框架。。那遇到那种又有 native,又有 rn 的应用,这个覆盖率是不是要分开来看待了?另外,你这个是测试用例的覆盖率,那手动测试的覆盖率怎么做呢?

思寒_seveniruby 将本帖设为了精华贴 10 Apr 13:42

类似 SDK 的 jar 包如何统计覆盖率?

恒温 回复

好问题,从技术角度,这两者的覆盖率从收集到生成报告都是两套不同的机制的。如何结合起来一起看,这是个难题。目前还没到这一步,不好回答,不过从我的角度,最终覆盖率的服务形式不是单纯让测试人员看覆盖率报告,而是从覆盖率报告得到测试的推荐建议,例如 xx 模块未覆盖,涉及 xx 流程,优先级 px ,建议通过 xx 用例覆盖。

手动测试的覆盖率是下一步目标,目前计划是用 istanbul-middleware 来做。具体可行性后续继续研究。

simple 回复

目前没在 sdk 实践过,以下仅为参考 android app 收集覆盖率得到的想法:

jacoco 原理上应该支持任何 java 程序的覆盖率收集和报告生成。可以考虑在 sdk 里面嵌入一个 receiver 执行覆盖率 ec 文件生成并存储的操作,然后另外做个 apk 来发送这个广播和把生成的文件上传到覆盖率后台服务进行报告生成。

其实,只要先定义好需要收集的覆盖数据,是代码行?条件覆盖?判定覆盖?还是独立路径覆盖?不同语言的覆盖报告也是可以整合来看的。我比较纠结的是一些配置文件的覆盖,像 mybatis 下的 SQL 语句配置 XML 文件,这些不好弄。

恩里科 回复

这个确实不好覆盖。不过如果这个 sql 不大复杂,确认有覆盖到这个 sql 对应的写入函数应该可以接受。如果 sql 很复杂,那得考虑用另外的方式去覆盖这块功能,例如单元测试。

不过我觉得没太大必要追求百分百的覆盖率,有个基础要求就好。对于手工测试这种场景,覆盖率数据的价值是根据未覆盖内容结合对代码的解析,得出未覆盖的功能或流程,然后由团队根据实际需要补充相关的测试用例,避免遗漏。

陈恒捷 回复

的确是没有必要百分百覆盖,做到七八十就已经很利害了。再上做花得时间成本太高,不划算。我的经验是关键路径、关键函数 100% 覆盖,其他的不作要求,只作数据记录。不过也就是要定个度量的单位,代码行、条件判定、独立路径选一个。不然给出的数据太多,容易分散看报告的人的注意力。

陈恒捷 React Native 代码覆盖率获取探索 (二) 中提及了此贴 05 Jun 23:44

@chenhengjie123 看完后,有点蒙蔽,这个能做 java 的接口覆盖率测试吗

陈恒捷 #11 · June 06, 2017 Author
刘扬 回复

这个是针对 javascript 的,java 可以用 jacoco ,社区也有一些这方面的文章。

陈恒捷 回复

嗯,我看了社区的一些关于 jacoco 的覆盖率测试,没找到想要的,你用过 jacoco 的,接口测试吗?而且开发代码和测试代码是分离开来的

陈恒捷 #13 · June 07, 2017 Author
刘扬 回复

你想要什么?

覆盖率的话,jacoco 离线插桩会需要在源码配置项里做一些变更的。如果是服务端在线插桩,可以做到源码不需要做任何变动。

接口测试的话,被测系统代码和接口测试代码分离,这不是很正常嘛,不是很明白你的问题点在哪里。

陈恒捷 回复

服务端在线插桩,比如在 tomcat 中配置一个
JAVA_OPTS="javaagent:/path/to/your/jacoco_0.6.4/lib/jacocoagent.jar=includes=com.baidu.*,output=tcpserver,port=8893,address=10.81.14.77 -Xverify:none"
这就是在线插桩呢??????

陈恒捷 回复

我说的被测系统代码和接口测试代码分离意思是:这俩代码不在同一个工程中,用 jacoco 标记了被测系统代码后,那怎么和接口测试代码管理来生成覆盖率报告呢?

陈恒捷 #16 · June 07, 2017 Author
刘扬 回复

恩。在线插桩可能说得有点高级了,jacoco 把这种方式叫做 on the fly

这种模式下,通过 ant 就可以直接从被测服务器获取、清空覆盖率数据和生成覆盖率报告了。你可以在你整个测试开始前执行 ant 相关的命令清空当前覆盖率数据,然后结束后执行这个 ant 命令来生成覆盖率报告。

陈恒捷 如何量化测试覆盖率 中提及了此贴 14 Sep 07:35

在哪个目录下执行 npm start, 为何执行时报错 missing script: start

陈恒捷 #19 · March 13, 2018 Author
jimmy_70258 回复

在 f8app 文件夹里。你先检查下有没有顺利跑完 npm install ?

陈恒捷 回复

仍提示 missing script: start

陈恒捷 #21 · March 14, 2018 Author

把完整步骤和日志贴下?

Author only
Author only
24Floor has deleted
Author only
陈恒捷 #26 · March 15, 2018 Author
jimmy_70258 回复

不好意思,这两天比较忙,没留意到消息。

可以分享下具体是怎么解决的吗?

您好,请教一下,获取到覆盖率报告后,你们有对每一个版本的报告数据进行数据持久化吗 这个是这么做的呢?

zhntester 回复

有持久化,不过当时做得比较简单,只是保存了生成的报告。增量覆盖、多版本(指的是开发可能调整了代码产生新版本这种)覆盖数据合并这些都没有做,所以不大好回答你怎么持久化比较好。

您好,我这个有覆盖率数据,但是就是没有样式,找不到问题的原因了

zhntester 回复

打开 chrome 的开发者工具,通过 network 看看是哪里有问题?

没有样式一般就是 css 加载出问题了。

恒温 回复

你们后面有做出来吗,我们的 App 也是既有 native 的也有 rn 的,还有 flutter 的...想问下这个咋做呀

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