手把手教你创建第一个React Native自动化测试工具Detox
| Detox([ˈdiˌtɑks])
Detox 是一个用于移动端 APP 灰盒测试(介于白盒测试和黑盒测试之间,既关注内部逻辑实现也关注软件最终效果,通常在集成测试阶段进行)的自动化测试框架。
Detox提供了清晰的api来获取引用和触发元素上的操作 示例代码:
describe('Login flow', () => {
it('should login successfully', async () => {
await device.launchApp();
// 通过ID获取元素的引用,并显示出来
await expect(element(by.id('email'))).toBeVisible();
// 获取引用并键入指
await element(by.id('email')).typeText('john@example.com');
await element(by.id('password')).typeText('123456');
// 获取引用并执行点击操作
await element(by.text('Login')).tap();
await expect(element(by.text('Welcome'))).toBeVisible();
await expect(element(by.id('email'))).toNotExist();
});
});
工作原理
功能
- 跨平台
- async-await异步断点调试
- 自动化同步
- 专为CI打造
- 支持在设备上运行
优点
- Detox支持Android和iOS。与React Native 在iOS和Android的代码几乎相同、可复用
- 支持各种Test runner, 比如Mocha(轻量级),Jest(推荐使用)等
- 代码侵入性小
- 搭建简单、运行的时候只需要detox build命令来编测试app和detox test来执行脚本即可
- 社区活跃
- 使用async-await同步执行异步任务
await element(by.id('ButtonA')).tap();
await element(by.id('ButtonB')).tap();
- api清晰、学习成本低、减少心智负担
缺点
- 在进程中执行了额外的代码来监听 App 的行为
- 无限重复的动画会让脚本一直处于等待状态,需要额外的代码让自动化测试的build去掉无限循环的动画
使用
默认您已经安装node以及对应的Android或IOS等相关环境
这里只介绍对应的Detox
安装使用
- 安装对应工具
// Command Line Tools
npm install detox-cli --global
// 添加到当前RN项目中
yarn add detox -D
将你的android文件放在Android studio中构建
- 更改android/build.gradle 文件
在allprojects.repositories中添加以下
maven {
// 所有 Detox 的模块都通过 npm 模块
url "$rootDir/../node_modules/detox/Detox-android"
}
buildscript的ext 中添加kotlinVersion字段
kotlinVersion = '1.6.21' // (check what the latest version is!)
在dependencies中添加
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
- 更改android/ app/build.gradle 文件
在中android.defaultConfig添加以下2行
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
在android.buildTypes.release中添加以下3行
minifyEnabled enableProguardInReleaseBuilds
// Typical pro-guard definitions
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
// Detox-specific additions to pro-guard
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
在dependencies中添加以下两行
// detox config
androidTestImplementation 'com.wix:detox:+'
androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1'
- android文件夹中创建对应的目录及文件
添加文件和目录到 android/app/src/androidTest/java/com/你的包名,全小写/DetoxTest.java
不要忘记将包名称更改为您的项目
将代码内容复制到DetoxText文件中
注意:复制后的内容需要处理一下
- detoxrc与e2e文件配置
输入detox init -r jest
生成e2e文件夹和.detoxrc.json文件
配置.detoxrc.json
文件
{
"testRunner": "jest",
"runnerConfig": "e2e/config.json",
"skipLegacyWorkersInjection": true,
"devices": {
"emulator": {
"type": "android.emulator",
"device": {
"avdName": "Nexus_S_API_28" // 设备名称 执行adb -s emulator-5554 emu avd name 获取
}
}
},
"apps": {
"android.debug": {
"type": "android.apk",
"binaryPath": "android/app/build/outputs/apk/debug/app-debug.apk",
"build": "cd android && gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd .."
},
"android.release": {
"type": "android.apk",
"binaryPath": "android/app/build/outputs/apk/release/app-release.apk",
"build": "cd android && gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd .."
}
},
"configurations": {
"android.emu.debug": {
"device": "emulator",
"app": "android.debug"
},
"android.emu.release": {
"device": "emulator",
"app": "android.release"
}
}
}
配置e2e/firstTest.e2e.js
文件
// eslint-disable-next-line no-undef
describe('Login flow test', () => {
beforeEach(async () => {
await device.launchApp();
// await device.reloadReactNative();
});
it('should have login screen', async () => {
await expect(element(by.id('loginView'))).toBeVisible();
});
it('should fill login form', async () => {
await element(by.id('usernameInput')).typeText('zzzzz');
await element(by.id('passwordInput')).typeText('test123\n');
await element(by.id('loginButton')).tap();
});
it('should show dashboard screen', async () => {
await expect(element(by.id('dashboardView'))).toBeVisible();
await expect(element(by.id('loginView'))).not.toExist();
});
});
在e2e中创建 随便命名xxx.spec.js
和随便命名xxx
文件夹
const parseSpecJson = specJson => {
describe(specJson.describe, () => {
for (let i = 0; i < specJson.flow.length; i++) {
const flow = specJson.flow[i];
it(flow.it, async () => {
for (let j = 0; j < flow.steps.length; j++) {
const step = flow.steps[j];
const targetElement = element(
by[step.element.by](step.element.value),
);
if (step.type === 'assertion') {
await expect(targetElement)[step.effect.key](step.effect.value);
} else {
await targetElement[step.effect.key](step.effect.value);
}
}
});
}
});
};
parseSpecJson(require('./随便命名xxx/login.json'));
在e2e/随便命名xxx
文件夹中创建login.json
{
"describe": "Login flow test",
"flow": [
{
"it": "should have login screen",
"steps": [
{
"type": "assertion",
"element": {
"by": "id",
"value": "loginView"
},
"effect": {
"key": "toBeVisible",
"value": ""
}
}
]
},
{
"it": "should fill login form",
"steps": [
{
"type": "action",
"element": {
"by": "id",
"value": "usernameInput"
},
"effect": {
"key": "typeText",
"value": "varunk"
}
},
{
"type": "action",
"element": {
"by": "id",
"value": "passwordInput"
},
"effect": {
"key": "typeText",
"value": "test123\n"
}
},
{
"type": "action",
"element": {
"by": "id",
"value": "loginButton"
},
"effect": {
"key": "tap",
"value": ""
}
}
]
},
{
"it": "should show dashboard screen",
"steps": [
{
"type": "assertion",
"element": {
"by": "id",
"value": "dashboardView"
},
"effect": {
"key": "toBeVisible",
"value": ""
}
},
{
"type": "assertion",
"element": {
"by": "id",
"value": "loginView"
},
"effect": {
"key": "toNotExist",
"value": ""
}
}
]
}
]
}
- 给你的组件加上testID
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow
*/
import React, {useState} from 'react';
import {
SafeAreaView,
StyleSheet,
ScrollView,
View,
Text,
StatusBar,
TextInput,
Button,
ActivityIndicator,
} from 'react-native';
import {Colors} from 'react-native/Libraries/NewAppScreen';
const LOGIN_STATUS = {
NOT_LOGGED_IN: -1,
LOGGING_IN: 0,
LOGGED_IN: 1,
};
const App: () => React$Node = () => {
const [loginData, setLoginData] = useState({
username: '',
password: '',
});
const [loginStatus, setLoginStatus] = useState(LOGIN_STATUS.NOT_LOGGED_IN);
const onLoginDataChange = key => {
return value => {
const newLoginData = Object.assign({}, loginData);
newLoginData[key] = value;
setLoginData(newLoginData);
};
};
const onLoginPress = () => {
setLoginStatus(LOGIN_STATUS.LOGGING_IN);
setTimeout(() => {
setLoginStatus(LOGIN_STATUS.LOGGED_IN);
}, 1500);
};
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={styles.container}>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.scrollView}>
{loginStatus === LOGIN_STATUS.LOGGED_IN ? (
<View testID="dashboardView">
<Text style={styles.heading} testID="dashboardHeadingText">
Hello {loginData.username}
</Text>
<Text style={[styles.link, styles.mt12]}>Edit your profile</Text>
</View>
) : (
<View testID="loginView">
<Text style={styles.heading}>Please Login</Text>
<Text style={styles.mt12}>Username</Text>
<TextInput
style={[styles.textInput, styles.mt12]}
placeholder={'Enter your username'}
onChangeText={onLoginDataChange('username')}
value={loginData.username}
testID="usernameInput"
/>
<Text style={styles.mt12}>Password</Text>
<TextInput
secureTextEntry
style={[styles.textInput, styles.mt12, styles.mb12]}
placeholder={'Enter your password'}
onChangeText={onLoginDataChange('password')}
value={loginData.password}
testID="passwordInput"
/>
<Button
title="Login"
onPress={onLoginPress}
testID="loginButton"
/>
{loginStatus === LOGIN_STATUS.LOGGING_IN ? (
<ActivityIndicator style={styles.mt12} />
) : null}
</View>
)}
</ScrollView>
</SafeAreaView>
</>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollView: {
backgroundColor: Colors.white,
padding: 16,
},
heading: {
textAlign: 'center',
fontSize: 18,
},
textInput: {
borderColor: Colors.lighter,
borderWidth: 1,
borderRadius: 4,
paddingLeft: 10,
paddingTop: 4,
paddingRight: 4,
paddingBottom: 4,
},
mt12: {
marginTop: 12,
},
mb12: {
marginBottom: 12,
},
link: {
color: '#3543bf',
},
});
export default App;
- 测试开始
- 打包app到模拟器上
- 执行
detox build --configuration android.emu.debug
- 执行
yarn start
- 执行
adb reverse tcp:8081 tcp:8081
- 执行
detox test -c android.emu.debug
执行detox test -c android.emu.debug
之后,项目开始运行测试如下
至此运行通过,测试结束
| 参考资料
# 如何自动化测试 React Native 项目 (上篇) - 核心思想与E2E自动化 # React Native end-to-end testing with Detox # e2e testing of react native app (Android) using detox — a step by step guide
转载自:https://juejin.cn/post/7153181302647160868