了解如何使用 Detox 等流行库在 EAS Build 上设置和运行 E2E 测试。
已弃用:这是我们的 EAS Build E2E 测试指南的旧存档版本。在此处查看指南的最新版本。
通过 EAS Build,你可以构建一个工作流程来为你的应用运行 E2E 测试。在本指南中,你将学习如何使用最流行的库之一 (排毒) 来做到这一点。
¥With EAS Build, you can build a workflow for running E2E tests for your application. In this guide, you will learn how to use one of the most popular libraries (Detox) to do that.
本指南解释了如何在裸工作流程项目中使用 Detox 运行 E2E 测试。你可以将 @config-plugins/detox
用于托管项目,但你可能需要调整本指南中的一些说明才能执行此操作。
¥This guide explains how to run E2E tests with Detox in a bare workflow project. You can use @config-plugins/detox
for a managed project, but you may need to adjust some of the instructions in this guide to do so.
¥Running iOS tests
1
¥Initialize a new bare workflow project
让我们首先初始化一个新的 Expo 项目,安装和配置 @config-plugins/detox
,然后运行 npx expo prebuild
来生成原生项目。
¥Let's start by initializing a new Expo project, installing and configuring @config-plugins/detox
, and running npx expo prebuild
to generate the native projects.
从以下命令开始:
¥Start with the following commands:
# Initialize a new project
-
npx create-expo-app eas-tests-example
# cd into the project directory
-
cd eas-tests-example
# Install @config-plugins/detox
-
npm install --save-dev @config-plugins/detox
现在,打开 app.json 并将 @config-plugins/detox
插件添加到 plugins
列表中(这必须在预构建之前完成)。这将自动配置 Android 原生代码以支持 Detox。
¥Now, open app.json and add the @config-plugins/detox
plugin to your plugins
list (this must be done before prebuilding). This will automatically configure the Android native code to support Detox.
{
"expo": {
// ...
"plugins": ["@config-plugins/detox"]
}
}
运行 prebuild 以生成原生项目:
¥Run prebuild to generate the native projects:
-
npx expo prebuild
2
¥Make home screen interactive
编写 E2E 测试的第一步是有一些东西要测试 - 有一个空的应用,所以让我们让我们的应用具有交互性。你可以添加一个按钮并在按下时显示一些新文本。稍后,你将编写一个测试来模拟点击按钮并验证文本是否按预期显示。
¥The first step to writing E2E tests is to have something to test — have an empty app, so let's make our app interactive. You can add a button and display some new text when it's pressed. Later, you will write a test that simulates tapping the button and verifies if the text is displayed as expected.
import { StatusBar } from 'expo-status-bar';
import { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
export default function App() {
const [clicked, setClicked] = useState(false);
return (
<View style={styles.container}>
{!clicked && (
<Pressable testID="click-me-button" style={styles.button} onPress={() => setClicked(true)}>
<Text style={styles.text}>Click me</Text>
</Pressable>
)}
{clicked && <Text style={styles.hi}>Hi!</Text>}
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
hi: {
fontSize: 30,
color: '#4630EB',
},
button: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 32,
borderRadius: 4,
elevation: 3,
backgroundColor: '#4630EB',
},
text: {
fontSize: 16,
lineHeight: 21,
fontWeight: 'bold',
letterSpacing: 0.25,
color: 'white',
},
});
3
¥Set up Detox
¥Install dependencies
让我们在项目中添加两个开发依赖 - jest
和 detox
。jest
(或 mocha
)是必需的,因为 detox
没有自己的测试运行程序。
¥Let's add two development dependencies to the project - jest
and detox
. jest
(or mocha
) is required because detox
does not have its own test-runner.
# Install jest and detox
-
npm install --save-dev jest detox
# Create Detox configuration files
-
npx detox init -r jest
请参阅官方 Detox 文档中的 入门 和 项目设置 指南,了解此过程的任何潜在更新。
¥See the Getting started and Project setup guides in official Detox documentation to learn about any potential updates to this process.
¥Configure Detox
Detox 要求你指定构建命令及其生成的二进制文件的路径。从技术上讲,在 EAS Build 上运行测试时,构建命令不是必需的,但允许你在本地运行测试(例如,使用 npx detox build --configuration ios.release
)。
¥Detox requires you to specify both the build command and the path to the binary produced by it. Technically, the build command is not necessary when running tests on EAS Build, but allows you to run tests locally (for example, using npx detox build --configuration ios.release
).
使用以下配置更新 .detoxrc.js:
¥Update .detoxrc.js with the following configuration with:
/** @type {Detox.DetoxConfig} */
module.exports = {
logger: {
level: process.env.CI ? 'debug' : undefined,
},
testRunner: {
$0: 'jest',
args: {
config: 'e2e/jest.config.js',
_: ['e2e'],
},
},
artifacts: {
plugins: {
log: process.env.CI ? 'failing' : undefined,
screenshot: 'failing',
},
},
apps: {
'ios.release': {
type: 'ios.app',
build:
'xcodebuild -workspace ios/eastestsexample.xcworkspace -scheme eastestsexample -configuration Release -sdk iphonesimulator -arch x86_64 -derivedDataPath ios/build',
binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/eastestsexample.app',
},
'android.release': {
type: 'android.apk',
build:
'cd android && ./gradlew :app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release && cd ..',
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
},
},
devices: {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPhone 14',
},
},
emulator: {
type: 'android.emulator',
device: {
avdName: 'pixel_4',
},
},
},
configurations: {
'ios.release': {
device: 'simulator',
app: 'ios.release',
},
'android.release': {
device: 'emulator',
app: 'android.release',
},
},
};
4
¥Write E2E tests
接下来,添加你的第一个 E2E 测试。删除自动生成的 e2e/starter.test.js 并使用以下代码片段创建我们自己的 e2e/homeScreen.test.js:
¥Next, add your first E2E tests. Delete the auto-generated e2e/starter.test.js and create our own e2e/homeScreen.test.js with the following snippet:
describe('Home screen', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('"Click me" button should be visible', async () => {
await expect(element(by.id('click-me-button'))).toBeVisible();
});
it('shows "Hi!" after tapping "Click me"', async () => {
await element(by.id('click-me-button')).tap();
await expect(element(by.text('Hi!'))).toBeVisible();
});
});
套件中有两个测试:
¥There are two tests in the suite:
检查 "点我" 按钮在主屏幕上是否可见。
¥One that checks whether the "Click me" button is visible on the home screen.
另一个验证点击按钮会触发显示 "你好!"。
¥Another that verifies that tapping the button triggers displaying "Hi!".
两个测试均假设按钮的 testID
设置为 click-me-button
。详细信息请参见 源代码。
¥Both tests assume the button has the testID
set to click-me-button
. See the source code for details.
5
¥Configure EAS Build
配置 Detox 并编写初始 E2E 测试后,下一步是配置 EAS Build 并在云中执行测试。
¥After configuring Detox and writing the initial E2E test, the next step is to configure EAS Build and execute the tests in the cloud.
¥Create eas.json
以下命令在项目根目录中创建 eas.json:
¥The following command creates eas.json in the project's root directory:
-
eas build:configure
¥Configure EAS Build
作为构建的一部分,还需要执行几个步骤来配置 EAS Build 以运行 E2E 测试:
¥There are a few more steps to configure EAS Build for running E2E tests as part of the build:
安卓测试:
¥Android test:
测试在 Android 模拟器中运行。你将定义一个构建配置文件,用于为模拟器构建应用(生成 .apk 文件)。
¥Tests are run in the Android Emulator. You will define a build profile that builds your app for the emulator (produces a .apk file).
安装模拟器及其所有系统依赖。
¥Install the emulator and all its system dependencies.
iOS 测试:
¥iOS test:
测试在 iOS 模拟器中运行。你将定义一个构建配置文件来为模拟器构建应用。
¥Tests are run in the iOS Simulator. You will define a build profile that builds your app for the simulator.
安装 applesimutils
命令行实用程序。
¥Install the applesimutils
command line util.
配置 EAS Build 以在成功构建应用后运行 Detox 测试。
¥Configure EAS Build to run Detox tests after successfully building the app.
编辑 eas.json 并添加 test
构建配置文件:
¥Edit eas.json and add the test
build profile:
{
"build": {
"test": {
"android": {
"gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release",
"withoutCredentials": true
},
"ios": {
"simulator": true
}
}
}
}
创建 eas-hooks/eas-build-pre-install.sh 来安装给定平台所需的工具和依赖:
¥Create eas-hooks/eas-build-pre-install.sh to install the necessary tools and dependencies for the given platform:
#!/usr/bin/env bash
set -eox pipefail
if [[ "$EAS_BUILD_RUNNER" == "eas-build" && "$EAS_BUILD_PROFILE" == "test"* ]]; then
if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
sudo apt-get --quiet update --yes
# Install emulator & video bridge dependencies
# Source: https://github.com/react-native-community/docker-android/blob/master/Dockerfile
sudo apt-get --quiet install --yes \
libc6 \
libdbus-1-3 \
libfontconfig1 \
libgcc1 \
libpulse0 \
libtinfo5 \
libx11-6 \
libxcb1 \
libxdamage1 \
libnss3 \
libxcomposite1 \
libxcursor1 \
libxi6 \
libxext6 \
libxfixes3 \
zlib1g \
libgl1 \
pulseaudio \
socat
# Emulator must be API 31 -- API 32 and 33 fail due to https://github.com/wix/Detox/issues/3762
sdkmanager --install "system-images;android-31;google_apis;x86_64"
avdmanager --verbose create avd --force --name "pixel_4" --device "pixel_4" --package "system-images;android-31;google_apis;x86_64"
else
brew tap wix/brew
brew install applesimutils
fi
fi
接下来,创建 eas-hooks/eas-build-on-success.sh 并添加代码片段,如下所示。该脚本针对 Android 和 iOS 运行不同的命令。
¥Next, create eas-hooks/eas-build-on-success.sh and add the code snippet as shown below. The script runs different commands for Android and iOS.
对于 Android,你必须在运行测试之前手动启动模拟器,因为 detox
偶尔会遇到独立启动模拟器的问题,这可能会导致套件的第一次测试期间挂起。完成 detox test
后,有一个命令可以杀死之前启动的模拟器。
¥For Android, you have to manually start the emulator before running the tests as detox
occasionally encounters issues initiating the emulator independently, which can cause a hang during the first test of your suite. After completing the detox test
, there is a command that kills the previously started emulator.
对于 iOS,唯一的命令是 detox test
。
¥For iOS, the only command is detox test
.
#!/usr/bin/env bash
function cleanup()
{
echo 'Cleaning up...'
if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
# Kill emulator
adb emu kill &
fi
}
if [[ "$EAS_BUILD_PROFILE" != "test" ]]; then
exit
fi
# Fail if anything errors
set -eox pipefail
# If this script exits, trap it first and clean up the emulator
trap cleanup EXIT
ANDROID_EMULATOR=pixel_4
if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
# Start emulator
$ANDROID_SDK_ROOT/emulator/emulator @$ANDROID_EMULATOR -no-audio -no-boot-anim -no-window -use-system-libs 2>&1 >/dev/null &
# Wait for emulator
max_retry=10
counter=0
until adb shell getprop sys.boot_completed; do
sleep 10
[[ counter -eq $max_retry ]] && echo "Failed to start the emulator!" && exit 1
counter=$((counter + 1))
done
# Execute Android tests
if [[ "$EAS_BUILD_PROFILE" == "test" ]]; then
detox test --configuration android.release
fi
else
# Execute iOS tests
if [[ "$EAS_BUILD_PROFILE" == "test" ]]; then
detox test --configuration ios.release
fi
fi
更新 package.json 以使用 EAS 构建钩子 在 EAS Build 上运行上述脚本:
¥Update package.json to use EAS Build hooks to run the above scripts on EAS Build:
{
"scripts": {
"eas-build-pre-install": "./eas-hooks/eas-build-pre-install.sh",
"eas-build-on-success": "./eas-hooks/eas-build-on-success.sh"
}
}
不要忘记为 eas-build-pre-install.sh 和 eas-build-on-success.sh 添加可执行权限。运行
chmod +x eas-hooks/*.sh
。¥Don't forget to add executable permissions to eas-build-pre-install.sh and eas-build-on-success.sh. Run
chmod +x eas-hooks/*.sh
.
6
¥Run tests on EAS Build
在 EAS Build 上运行测试就像运行常规构建一样:
¥Running the tests on EAS Build is like running a regular build:
-
eas build -p all -e test
如果你已正确设置所有内容,你应该在构建日志中看到成功的测试结果:
¥If you have set up everything correctly you should see the successful test result in the build logs:
7
¥Upload screenshots of failed test cases
此步骤是可选的,但强烈推荐。
¥This step is optional but highly recommended.
当 E2E 测试用例失败时,查看应用状态的屏幕截图会很有帮助。EAS Build 可以轻松地使用 eas.json 中的 buildArtifactPaths
上传任意构建工件。
¥When an E2E test case fails, it can be helpful to see the screenshot of the application state. EAS Build makes it easy to upload any arbitrary build artifacts using buildArtifactPaths
in eas.json.
¥Take screenshots for failed tests
Detox 支持在测试中截取设备的屏幕截图。上面的 .detoxrc.js 示例 包括 Detox 的配置,用于拍摄失败测试的屏幕截图。
¥Detox supports taking in-test screenshots of the device. The .detoxrc.js sample above includes a configuration for Detox to take screenshots of failed tests.
¥Configure EAS Build for screenshots upload
编辑 eas.json 并将 buildArtifactPaths
添加到 test
构建配置文件中:
¥Edit eas.json and add buildArtifactPaths
to the test
build profile:
{
"build": {
"test": {
"android": {
"gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release",
"withoutCredentials": true
},
"ios": {
"simulator": true
},
"buildArtifactPaths": ["artifacts/**/*.png"]
}
}
}
与 applicationArchivePath
相比,即使构建失败,也会上传 buildArtifactPaths
中定义的构建工件。工件目录中的所有 .png 文件都将打包到 tarball 中并上传到 AWS S3。你可以稍后从构建详细信息页面下载它们。
¥In contrast to applicationArchivePath
, the build artifacts defined at buildArtifactPaths
will be uploaded even if the build fails. All .png files from the artifacts directory will be packed into a tarball and uploaded to AWS S3. You can download them later from the build details page.
如果你在本地运行 E2E 测试,请记住将工件添加到 .gitignore:
¥If you run E2E tests locally, remember to add artifacts to .gitignore:
artifacts/
¥Break a test and run a build
为了测试新配置,让我们进行测试并查看 EAS Build 上传屏幕截图。
¥To test the new configuration, let's break a test and see that EAS Build uploads the screenshots.
编辑 e2e/homeScreen.test.js 并进行以下更改:
¥Edit e2e/homeScreen.test.js and make the following change:
使用以下命令运行 iOS 构建并等待其完成:
¥Run an iOS build with the following command and wait for it to finish:
-
eas build -p ios -e test
进入构建详细信息页面后,你应该会看到构建失败。使用 "下载工件" 按钮下载并检查屏幕截图:
¥After going to the build details page you should see that the build failed. Use the "Download artifacts" button to download and examine the screenshot:
¥Repository
GitHub 上提供了本指南的完整示例。
¥Alternative approaches
¥Using development builds to speed up test run time
上述指南解释了如何针对项目的发布版本运行 E2E 测试,这需要在每次测试运行之前执行完全原生构建。如果仅项目 JavaScript 或资源发生了更改,则每次运行 E2E 测试时重新构建原生应用可能并不理想。但是,这对于发布版本是必需的,因为应用 JavaScript 包已嵌入到二进制文件中。
¥The above guide explains how to run E2E tests against a release build of your project, which requires executing a fully native build before each test run. Re-building the native app each time you run E2E tests may not be desirable if only the project JavaScript or assets have changed. However, this is necessary for release builds because the app JavaScript bundle is embedded into the binary.
相反,使用 开发构建 从本地开发服务器或从 已发布的更新 加载,以节省时间和 CI 资源。这可以通过让 E2E 测试运行程序使用指向特定更新包 URL 的 URL 调用应用来完成,如 开发构建深度链接 URL 指南.1 中所述。
¥Instead, use development builds to load from a local development server or from published updates to save time and CI resources. This can be done by having your E2E test runner invoke the app with a URL that points to a specific update bundle URL, as described in the development builds deep linking URLs guide.
首次启动应用时,开发版本通常会显示入门欢迎屏幕,旨在为开发者提供有关 expo-dev-client
UI 的上下文。但是,它可能会干扰你的 E2E 测试(期望与你的应用交互,而不是与入门屏幕交互)。要在测试环境中跳过入门屏幕,可以将查询参数 disableOnboarding=1
附加到项目 URL(EAS 更新 URL 或本地开发服务器 URL)。
¥Development builds typically display an onboarding welcome screen when an app is launched for the first time, which intends to provide context about the expo-dev-client
UI for developers. However, it can interfere with your E2E tests (which expect to interact with your app and not an onboarding screen). To skip the onboarding screen in a test environment, the query parameter disableOnboarding=1
can be appended to the project URL (an EAS Update URL or a local development server URL).
此类排毒测试的示例如下所示。完整的示例代码可在 eas-tests-example
存储库中找到。
¥An example of such a Detox test is shown below. Full example code is available on the eas-tests-example
repository.
const { openApp } = require('./utils/openApp');
describe('Home screen', () => {
beforeEach(async () => {
await openApp();
});
it('"Click me" button should be visible', async () => {
await expect(element(by.id('click-me-button'))).toBeVisible();
});
it('shows "Hi!" after tapping "Click me"', async () => {
await element(by.id('click-me-button')).tap();
await expect(element(by.text('Hi!'))).toBeVisible();
});
});
const appConfig = require('../../app.json');
const { resolveConfig } = require('detox/internals');
const platform = device.getPlatform();
module.exports.openApp = async function openApp() {
const config = await resolveConfig();
if (config.configurationName.split('.')[1] === 'debug') {
return await openAppForDebugBuild(platform);
} else {
return await device.launchApp({
newInstance: true,
});
}
};
async function openAppForDebugBuild(platform) {
const deepLinkUrl = process.env.EXPO_USE_UPDATES
? // Testing latest published EAS update for the test_debug channel
getDeepLinkUrl(getLatestUpdateUrl())
: // Local testing with packager
getDeepLinkUrl(getDevLauncherPackagerUrl(platform));
if (platform === 'ios') {
await device.launchApp({
newInstance: true,
});
sleep(3000);
await device.openURL({
url: deepLinkUrl,
});
} else {
await device.launchApp({
newInstance: true,
url: deepLinkUrl,
});
}
await sleep(3000);
}
const getDeepLinkUrl = url =>
`eastestsexample://expo-development-client/?url=${encodeURIComponent(url)}`;
const getDevLauncherPackagerUrl = platform =>
`http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1`;
const getLatestUpdateUrl = () =>
`https://u.expo.dev/${getAppId()}?channel-name=test_debug&disableOnboarding=1`;
const getAppId = () => appConfig?.expo?.extra?.eas?.projectId ?? '';
const sleep = t => new Promise(res => setTimeout(res, t));
/** @type {Detox.DetoxConfig} */
module.exports = {
logger: {
level: process.env.CI ? 'debug' : undefined,
},
testRunner: {
$0: 'jest',
args: {
config: 'e2e/jest.config.js',
_: ['e2e'],
},
},
artifacts: {
plugins: {
log: process.env.CI ? 'failing' : undefined,
screenshot: 'failing',
},
},
apps: {
'ios.debug': {
type: 'ios.app',
build:
'xcodebuild -workspace ios/eastestsexample.xcworkspace -scheme eastestsexample -configuration Debug -sdk iphonesimulator -arch x86_64 -derivedDataPath ios/build',
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/eastestsexample.app',
},
'ios.release': {
type: 'ios.app',
build:
'xcodebuild -workspace ios/eastestsexample.xcworkspace -scheme eastestsexample -configuration Release -sdk iphonesimulator -arch x86_64 -derivedDataPath ios/build',
binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/eastestsexample.app',
},
'android.debug': {
type: 'android.apk',
build:
'cd android && ./gradlew :app:assembleDebug :app:assembleAndroidTest -DtestBuildType=debug && cd ..',
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
},
'android.release': {
type: 'android.apk',
build:
'cd android && ./gradlew :app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release && cd ..',
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
},
},
devices: {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPhone 14',
},
},
emulator: {
type: 'android.emulator',
device: {
avdName: 'pixel_4',
},
},
},
configurations: {
'ios.debug': {
device: 'simulator',
app: 'ios.debug',
},
'ios.release': {
device: 'simulator',
app: 'ios.release',
},
'android.debug': {
device: 'emulator',
app: 'android.debug',
},
'android.release': {
device: 'emulator',
app: 'android.release',
},
},
};
#!/usr/bin/env bash
function cleanup()
{
echo 'Cleaning up...'
if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
# Kill emulator
adb emu kill &
fi
}
if [[ "$EAS_BUILD_PROFILE" != "test"* ]]; then
exit
fi
# Fail if anything errors
set -eox pipefail
# If this script exits, trap it first and clean up the emulator
trap cleanup EXIT
ANDROID_EMULATOR=pixel_4
if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then
# Start emulator
$ANDROID_SDK_ROOT/emulator/emulator @$ANDROID_EMULATOR -no-audio -no-boot-anim -no-window -use-system-libs 2>&1 >/dev/null &
# Wait for emulator
max_retry=10
counter=0
until adb shell getprop sys.boot_completed; do
sleep 10
[[ counter -eq $max_retry ]] && echo "Failed to start the emulator!" && exit 1
counter=$((counter + 1))
done
# Execute Android tests
if [[ "$EAS_BUILD_PROFILE" == "test" ]]; then
detox test --configuration android.release
fi
if [[ "$EAS_BUILD_PROFILE" == "test_debug" ]]; then
detox test --configuration android.debug
fi
else
# Execute iOS tests
if [[ "$EAS_BUILD_PROFILE" == "test" ]]; then
detox test --configuration ios.release
fi
if [[ "$EAS_BUILD_PROFILE" == "test_debug" ]]; then
detox test --configuration ios.debug
fi
fi
{
"build": {
"test": {
"android": {
"gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release",
"withoutCredentials": true
},
"ios": {
"simulator": true
}
},
"test_debug": {
"android": {
"gradleCommand": ":app:assembleDebug :app:assembleAndroidTest -DtestBuildType=debug",
"withoutCredentials": true
},
"ios": {
"buildConfiguration": "Debug",
"simulator": true
},
"env": {
"EXPO_USE_UPDATES": "1"
},
"channel": "test_debug"
}
}
}