Overview
If you prefer to read TypeScript files instead of docs, feel free to browse through the typings file provided by Detox.
Running automated tests on your mobile application implies firstly that you have a mobile device and an application. Unlike unit tests, where passing tests don’t require extra checks, the end-to-end tests are better to be executed multiple times – on various devices and app flavors (e.g. debug and release).
This is why Detox is inclined towards static configuration files describing permutations of apps, devices and a lot more, actually. If you come to think of it, even a simple React Native application is likely to have four combinations:
- iOS simulator running a debug build;
- iOS simulator running a release build;
- Android emulator running a debug build;
- Android emulator running a release build;
Imagine you want to check your app both on phones and tablets, and now you have eight combinations already! Add multiple OS versions into the test coverage matrix equation, and the configuration grows by a factor of N.
Config structure
In view of the arguments above, our recommendation to keep Detox configs
neat and tidy is to keep them inside three key-value dictionaries:
devices
, apps
and configurations
. See the schematic code below:
/* @type {Detox.DetoxConfig} */
module.exports = {
devices: {
device1: { /* ... */ },
device2: { /* ... */ },
},
apps: {
app1: { /* ... */ },
app2: { /* ... */ },
},
configurations: {
'device1+app1': {
device: 'device1',
app: 'app1',
},
/* ... */
},
};
Configuration names serve as an entry point, when you want to run Detox tests:
detox test -c 'device1+app1'
When Detox starts, it picks the specified configuration and resolves its aliases to the device and the application. However, the config file is not limited only to devices and applications – there are more sections:
/* @type {Detox.DetoxConfig} */
module.exports = {
artifacts: { /* ... */ },
behavior: { /* ... */ },
logger: { /* ... */ },
session: { /* ... */ },
testRunner: { /* ... */ },
devices: { /* ... */ },
apps: { /* ... */ },
configurations: { /* ... */ },
};
When the config gets finally resolved, it looks more like a flat structure, as shown on the diagram:
Aside from mandatory device
and app
properties, each configuration can have
overrides to the global config sections such as testRunner
, artifacts
,
behavior
and others, e.g.:
/* @type {Detox.DetoxConfig} */
module.exports = {
logger: {
level: process.env.CI ? 'debug' : 'info',
},
/* ... */
configurations: {
'ios.sim.debug': {
device: 'iphone',
app: 'ios.debug',
testRunner: {
args: {
runInBand: true,
},
},
// ...
logger: {
level: 'trace' // override
},
},
},
};
For more clarity, this relationship might be illustrated with a diagram:
It should be noted that the aliasing of devices and apps is optional in fact. Instead of using keys, you can inline both device and app configs into your configuration, e.g.:
/* @type {Detox.DetoxConfig} */
module.exports = {
configurations: {
'ios.sim.debug': {
device: {
type: 'ios.simulator',
device: {
type: 'iPhone 13 Pro',
},
},
app: {
type: 'ios.app',
binaryPath: '/path/to/your.app',
},
},
},
};
Besides, there is basic support for tests with multiple applications, if you switch
to apps
array (aliased or inlined) instead of app
, e.g.:
/* @type {Detox.DetoxConfig} */
module.exports = {
apps: { /* ... */ },
devices: { /* ... */ },
configurations: {
'multi.ios.debug': {
device: 'iphone',
apps: ['passenger.ios.debug', 'driver.ios.debug'],
},
},
};
Path conventions
Detox supports standalone configuration files and the respective named section inside package.json
.
It starts scanning from the current working directory, and runs over the following options, in this order:
.detoxrc.js
.detoxrc.json
.detoxrc
(JSON)detox.config.js
detox.config.json
package.json
If you decide to have detox
section in your package.json
, it should be defined as a top-level
property:
{
"name": "your-project",
"version": "X.Y.Z",
"scripts": {},
"detox": {
"devices": {},
"apps": {},
"configurations": {},
}
}
Extending
All Detox config files are extensible by definition. That helps if you ever need to share certain settings across multiple mobile projects, e.g.:
{
"extends": "@my-org/detox-preset",
"configurations": {
// …
},
}
Please note that extends
has to be a valid Node module path. Relative module paths will be resolved relatively
to the Detox config file which contains that specific extends
property, e.g.:
module.exports = { extends: "../base.detox.config.js" };
// the path resolves to: ~/Projects/my-project/base.detox.config.js
module.exports = { extends: "./ci.detox.config.js" };
// the path resolves to: ~/Projects/my-project/e2e/ci.detox.config.js
The extension chain can have an arbitrary length. All the configs are going to be deep-merged in the logical order: grandparent ← parent ← child.
Default configuration
As you might have noticed, you always have to pass -c <configuration name>
argument when running Detox tests:
detox test -c ios.sim.debug
Technically this is not true. You can omit the configuration name if:
there is only one configuration in
configurations
dictionary;you set some configuration as a default choice via
selectedConfiguration
property:/* @type {Detox.DetoxConfig} */
module.exports = {
selectedConfiguration: 'device1+app1',
devices: {
device1: { /* ... */ },
device2: { /* ... */ },
},
apps: {
app1: { /* ... */ },
app2: { /* ... */ },
},
configurations: {
'device1+app1': {
device: 'device1',
app: 'app1',
},
/* ... */
},
};
The next articles will be describing each configuration section in detail.