手把手教你建立第一個React Native自動化測試工具Detox
| Detox([ˈdiˌtɑks])
Detox 是一個用於移動端 APP 灰盒測試(介於白盒測試和黑盒測試之間,既關注內部邏輯實現也關注軟體最終效果,通常在整合測試階段進行)的自動化測試框架。
Detox提供了清晰的api來獲取引用和觸發元素上的操作 示例程式碼: ```javascript 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('[email protected]');
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同步執行非同步任務
javascript await element(by.id('ButtonA')).tap(); await element(by.id('ButtonB')).tap();
-
api清晰、學習成本低、減少心智負擔
缺點
- 在程序中執行了額外的程式碼來監聽 App 的行為
- 無限重複的動畫會讓指令碼一直處於等待狀態,需要額外的程式碼讓自動化測試的build去掉無限迴圈的動畫
使用
預設您已經安裝node以及對應的Android或IOS等相關環境
這裡只介紹對應的Detox
安裝使用
- 安裝對應工具 ```javascript // Command Line Tools npm install detox-cli --global
// 新增到當前RN專案中 yarn add detox -D ``` 將你的android檔案放在Android studio中構建
- 更改android/build.gradle 檔案
在allprojects.repositories中新增以下
javascript
maven {
// 所有 Detox 的模組都通過 npm 模組
url "$rootDir/../node_modules/detox/Detox-android"
}
buildscript的ext 中新增kotlinVersion欄位
javascript
kotlinVersion = '1.6.21' // (check what the latest version is!)
在dependencies中新增
javascript
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
- 更改android/ app/build.gradle 檔案
在中android.defaultConfig新增以下2行
javascript
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
在android.buildTypes.release中新增以下3行
javascript
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中新增以下兩行
javascript
// 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
檔案
javascript
{
"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
檔案
```javascript
// 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`資料夾
javascript
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(
bystep.element.by,
);
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`
javascript
{
"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 ```javascript /**
- Sample React Native App
- http://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 (
<>