Testing is always something that's important in app development, but often it's still done manually by testers or developers. Sometimes it can occur that one of the main workflows, e.g. the onboarding or sign up, fails in production because it hasn't been tested before. A couple of weeks ago, we've found Maestro which can be used for automating UI tests in a simple and effective way.
We want to show you in this article how you can integrate Maestro with a React Native app. For achieving this we will built a small application showing a sign in form and explain some core features of Maestro.
Developing the example app
Initializing the app
This first step is to create a React Native app. Here, you can select between the Expo and React Native CLI approach. We will create this tutorial for both approaches, because there are some special notes about creating flows with Maestro for Expo apps. The React Native CLI code can be found here. The Expo app code can be found here.
# Create React Native app with React Native CLI
$ npx react-native init ReactNativeMaestroExample --template react-native-template-typescript
# Create React Native app with Expo
$ npx create-expo-app -t expo-template-blank-typescript react-native-maestro-example
Changing the app identifier
One requirement for running the flows that we will create is that the app package name/ bundle identifier is com.example.app
. Here you can find an instruction for changing it in a React Native CLI app. In Expo apps, you need to set ios.bundleIdentifier
and android.package
properties in the app.json
file. Further docs for iOS and Android can be found in the Expo docs.
{
/** further config **/
"ios": {
"bundleIdentifier": "com.example.app"
},
"android": {
"package": "com.example.app"
}
}
Implementing components
Our example app with contain a single screen showing a sign in form with two inputs and a sign in button. If the username and password entered are valid (longer than 8 characters) a success message will be displayed below the sign in button. For implementing this, we use basic React Native components and style them using the StyleSheet. Below your can find the code of this sign in form.
export default function App() {
const [username, setUsername] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [valid, setValid] = useState(false);
return (
<View style={styles.container}>
<View style={styles.inputGroup}>
<Text style={styles.label}>Username</Text>
<TextInput
style={styles.input}
placeholder="Username"
onChange={(e) => setUsername(e.nativeEvent.text)}
testID="usernameInput"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Password</Text>
<TextInput
style={styles.input}
placeholder="Password"
secureTextEntry
onChange={(e) => setPassword(e.nativeEvent.text)}
testID="passwordInput"
/>
</View>
<TouchableOpacity
style={styles.button}
onPress={() => {
if (username.length > 0 && password.length > 0) {
setValid(true);
} else {
setValid(false);
}
}}
testID="signInButton"
>
<Text style={styles.buttonText}>Sign in</Text>
</TouchableOpacity>
{valid ? <Text style={styles.successText}>Sign in successfully</Text> : null}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 24,
backgroundColor: "#fff",
justifyContent: "center",
},
inputGroup: {
marginBottom: 16,
},
label: {
fontSize: 12,
fontWeight: "bold",
marginBottom: 6,
},
input: {
borderRadius: 8,
borderWidth: 1,
borderColor: "gray",
padding: 12,
width: "100%",
fontSize: 18,
},
button: {
borderRadius: 8,
padding: 12,
backgroundColor: "black",
alignItems: "center",
marginBottom: 8,
},
buttonText: {
fontSize: 16,
fontWeight: "bold",
color: "white",
},
successText: {
fontSize: 16,
color: "green",
alignSelf: 'center',
},
});
Installing maestro
Maestro will be installed using the terminal, because it will be used with the CLI. Installation for Mac OS and Linux requires just to commands. A detailed instruction about installing and upgrading Maestro can be found in the Maestro docs.
# install and upgrade maestro
$ curl -Ls "https://get.maestro.mobile.dev" | bash
Running flows on iOS Simulator requires installation of Facebook IDB.
$ brew tap facebook/fb
$ brew install facebook/fb/idb-companion
The installation of Maestro on Windows is a bit more complex. The detailed instruction can be found in the Maestro docs.
You can check if maestro is installed by checking the version. It should print the version number, e.g. 1.17.
$ maestro -v
Creating test flows
Our workflow should look like this:
- Start the app
- Enter username
- Enter password
- Press sign in button
- Check if success message is visible
Maestro studio
Maestro studio can be used for inspecting the hierarchy of your app. When opening Maestro studio a new window in your browser will be opened showing a screenshot of your app and the clickable elements.
$ maestro studio
Example workflow "sign in"
The first step in our workflow is to start the app. For this we can use start launchApp command. Here you can add further options like clearing the state, clearing the keychain or stop the app before launching. We don't need them in our example.
appId: com.example.app
---
- launchApp:
appId: com.example.app
The next step is to tap on the text field for entering the username. Maestro studio shows that we can achieve this by using one of the following commands.
The easiest one looks like this:
# tap on username
- tapOn:
text: "Username"
index: 1
The next step is to enter a user name. For Text Input, maestro provides different possibilities. One would be to enter a static text. This text could be as well defined as constant or passed as parameter. In addition, Maestro provides some random text that can be entered, e.g. email, person name, number or text.
# enter text
- inputText: "Hello World"
# enter random email
- inputRandomEmail
# enter random person name
- inputRandomPersonName
# enter random integer
- inputRandomNumber
# enter random text
- inputRandomText
After entering the text the keyboard needs to be closed that no component is displayed below the keyboard. This can be achieved using hideKeyboard. This is the part for entering the username by focusing the text field, entering a random person name and hiding the keyboard. Nearly the same thing needs to be done for the password as well. Here, we just need to replace Username text with Password.
# tap on username
- tapOn:
text: "Username"
index: 1
# enter username
- inputRandomPersonName
# hide keyboard
- hideKeyboard
After entering username and password, clicking the sign in button is the next step. This can be done as well using the tapOn. Here we pass the text of the button.
# tap on sign in
- tapOn: "Sign in"
# alternative: tap on sign in (does the same)
- tapOn:
text: "Sign in"
The final step is to check if sign up was successful by checking if the success message is displayed. This can be done using Assertions. Assertions help to check if an element is visible or not. The result can be used as well for running conditional flows.
The success message in our example app can be checked with this:
- assertVisible: "Sign in successfully"
The complete workflow is shown below.
appId: com.example.app
---
- launchApp:
appId: com.example.app
# tap on username
- tapOn:
text: "Username"
index: 1
# enter username
- inputRandomPersonName
# hide keyboard
- hideKeyboard
# tap on password
- tapOn:
text: "Password"
index: 1
# enter password
- inputRandomText
# hide keyboard
- hideKeyboard
# tap on sign in
- tapOn: "Sign in"
- assertVisible: "Sign in successfully"
Further actions that can be executed during the flow can be found in the Maestro documentation.
Running workflows
Run workflow local
After finishing the development of the workflow we can run it locally using maestro test
. Here you can specify if you want to run a single workflow or all workflows in a specific directory.
# run single flow
$ maestro test .maestro/sign-in-flow.yaml
# run all flows in a directory
$ maestro test .maestro
Run workflow in the cloud
After signing up for maestro cloud, you can run your tests as well in the cloud. This cloud execution will be useful if you want to run your workflows in your CI/ CD pipeline.
$ maestro cloud --apiKey <apiKey> <appFile> .maestro/
Tips and tricks
Using testID property
Writing test flows is really easy and straight forward. One problem comes up if you want to test your application in different languages. Normally, you would need to create a test flow for each languages which increases maintainability and development time with every new language.
For this problem you can use the testID
property which is provided for buttons, texts, views, images and other components. This property is mapped to the id
property in Maestro. After integrating the testID
you an access an element, like this:
# using testID property
- tapOn:
id: "signInButton"
# using text
- tapOn:
text: "Username"
index: 1
This makes it less dependent on the text or translations shown in your app. In some libraries using the testID
property is not working, but you will find this out while inspecting the hierarchy of your app.
<TextInput
style={styles.input}
placeholder="Password"
secureTextEntry
onChange={(e) => setPassword(e.nativeEvent.text)}
testID="passwordInput"
/>
Furthermore using the testID
makes it more easy to find the component in your app.
Developing flows with Expo
Developing maestro test flows with Expo is a bit different compared to development for React Native apps created with the React Native CLI. The difference is that your app built with Expo is started within the Expo Go App. This means that you're not able to start your test workflow by identifying your app with the bundle identifier. If you would do so, the workflow would fail. Instead you can open your app with openLink from Maestro. Here you just need to enter the url which is exposed while starting your expo app locally. As well you can just use your localhost for doing this, e.g. exp://127.0.0.1:19000. You need to keep in mind that the localhost would not work if you connect your smartphone via USB to your computer or laptop.
# common way to start your app
- launchApp:
appId: com.example.app
# needed to open app in expo
- openLink: exp://127.0.0.1:19000
Some examples for creating a workflow using expo can be found in our example repository
Recording flows
Another feature provided by Maestro is recording flows. Here you will get a screen recording of your device where the flow is executed. Next to it a terminal window with the executed steps is shown. When the record is finished, you will see a link in your terminal window that can be used for downloading the video as mp4 file.
# start video recording
$ maestro record flow-file.yaml
Below you can find an example recording.
Variables and parameters
Instead of using random text like in the example at the top, you can use parameters and constants for passing data to your flow. One way is to push data with external parameters into your flow. These parameters will be defined when starting your workflow in the terminal. Below you can find the sign-in workflow updated with external parameters for USERNAME and PASSWORD.
appId: com.example.app
---
- launchApp:
appId: com.example.app
# tap on username
- tapOn:
id: "usernameInput"
# enter username
- inputText: ${USERNAME}
# hide keyboard
- hideKeyboard
# tap on password
- tapOn:
id: "passwordInput"
# enter password
- inputText: ${PASSWORD}
# hide keyboard
- hideKeyboard
# tap on sign in
- tapOn:
id: "signInButton"
- assertVisible: "Sign in successfully"
You can run this workflow like this
$ maestro test -e USERNAME="Test User" -e PASSWORD=Test123456 .maestro/sign-in-flow-external-parameters.yaml
Instead of passing the data as external parameters, you can define them as well as constants in your flow. The only changed we need to do is to define the constants at the top of the workflow file below the appId. The syntax for accessing the variables stays the same.
appId: com.example.app
env:
USERNAME: "Test User"
PASSWORD: Test123456
---
- launchApp:
appId: com.example.app
# tap on username
- tapOn:
id: "usernameInput"
# enter username
- inputText: ${USERNAME}
# hide keyboard
- hideKeyboard
# tap on password
- tapOn:
id: "passwordInput"
# enter password
- inputText: ${PASSWORD}
# hide keyboard
- hideKeyboard
# tap on sign in
- tapOn:
id: "signInButton"
- assertVisible: "Sign in successfully"
This workflow can be started like any other flow without any external parameters.
$ maestro test .maestro/sign-in-flow-constants.yaml
One example for using the external parameters will be passing the app id if you have different app ids for different environments, e.g. development, alpha and production. With replacing the app id as external parameter you can reuse your flows for different app versions.
Nested flows
Nested flows can help to move recurring flows, e.g. sign-in or entering text, into separate flow files for reducing the complexibility of flows and increasing the maintainability.
Creating nested flows works the same way like normal flows. We will create a nested flow for entering the text in the sign in flow. This flow consists of three steps: focusing text input, entering text and hiding keyboard.
# tap on username
- tapOn:
id: "usernameInput"
# enter username
- inputRandomPersonName
# hide keyboard
- hideKeyboard
Moving this into a subflow looks like this. We will use external parameters for passing the text input id and the text.
appId: com.example.app
---
# tap on text input
- tapOn:
id: ${TEXT_INPUT_ID}
# enter text
- inputText: ${TEXT}
# hide keyboard
- hideKeyboard
When integrating this subflow into the main flow, we can use the runFlow command with passing external parameters as env. The subflow will be defined as file.
# enter username
- runFlow:
file: subflow-enter-text.yaml
env:
TEXT_INPUT_ID: usernameInput
TEXT: "Test User"
The complete sign in flow will look like this after integrating the subflows.
appId: com.example.app
---
- launchApp:
appId: com.example.app
# enter username
- runFlow:
file: subflows/subflow-enter-text.yaml
env:
TEXT_INPUT_ID: usernameInput
TEXT: "Test User"
# enter password
- runFlow:
file: subflows/subflow-enter-text.yaml
env:
TEXT_INPUT_ID: passwordInput
TEXT: Test123456
# tap on sign in
- tapOn:
id: "signInButton"
- assertVisible: "Sign in successfully"
Exporting test report
# creates test report in console
$ maestro test --format junit .maestro/sign-in-flow-testid.yaml
# creates test report as file
$ maestro test --format junit --output result.xml .maestro/sign-in-flow-testid.yaml
The result will look like this.
<?xml version='1.0' encoding='UTF-8'?>
<testsuites>
<testsuite name="Test Suite" device="iPhone 14 Plus - iOS 16.1 - E7F8022E-939F-4165-B887-F342740BFCE6" tests="1" failures="0">
<testcase id="sign-in-flow-testid" name="sign-in-flow-testid"/>
</testsuite>
</testsuites>
The result provides a good overview about the test results when running multiple workflows. We can do this by running every test in the React Native CLI example.
$ maestro test --format junit --output results.xml -e USERNAME="Test User" -e PASSWORD="Test123456" .maestro
The result for multiple workflows looks like this.
<?xml version='1.0' encoding='UTF-8'?>
<testsuites>
<testsuite name="Test Suite" device="iPhone 14 Plus - iOS 16.1 - E7F8022E-939F-4165-B887-F342740BFCE6" tests="5" failures="0">
<testcase id="sign-in-flow" name="sign-in-flow"/>
<testcase id="sign-in-flow-with-subflow" name="sign-in-flow-with-subflow"/>
<testcase id="sign-in-flow-constants" name="sign-in-flow-constants"/>
<testcase id="sign-in-flow-testid" name="sign-in-flow-testid"/>
<testcase id="sign-in-flow-external-parameters" name="sign-in-flow-external-parameters"/>
</testsuite>
</testsuites>
Top comments (12)
That is untreatable! Definitely, I want to move away from Appium.
Do you know how we can launch app with custom arguments?
Sounds great, I can really recommend Maestro.
You can use the parameters from maestro, e.g.
You can access those external parameters like this in your workflow file:
You can find an example workflow file with external parameters here github.com/alexanderhodes/react-na...
And I can recommend the documentation about parameters in the maestro docs maestro.mobile.dev/advanced/parame...
No you don't understood. I need support of launchArguments to access them inside my app (not inside e2e flow)
usigin a github.com/iamolegga/react-native-... module
Thank you for the post! I'm playing with this myself now because Appium has way too many problems.
What are the options for infrastructure to run Maestro? Is it currently only their Maestro Cloud? What about physical device support?
You can easily run it on their cloud, because they provide their a dashboard for your historical runs as well.
And you can run it on every infrastructure as well. You just need to install the CLI and the dependencies for the simulator or emulator. It's just a combination of installing the CLI like here.
Furthermore, you can run it on physical devices as well. On Android it works out of the box after connecting your device and for iOS device you need to install the Facebook IDB tool. The installation is described here: maestro.mobile.dev/getting-started...
And here you can find some notes about running it on physical devices. maestro.mobile.dev/getting-started...
Thanks for this, I've just started using maestro and this has some useful examples
Will be great to add that all
MAESTRO_*
env variabled will be available in flow without passing them as-e MY_VAR=myVal
and then
${MAESTRO_USERNAME} # 123
Another cool feature that have to mentioned it is conditions
How do you create the
.app
file for a ReactNative app so that it doesn't require Maestro?How to run on my local real devices (actual mobile devices) instead of virtual devices
At the moment, Maestro does not support real iOS devices
maestro.mobile.dev/getting-started...
Android - maestro test will automatically detect and use any local emulator or USB-connected physical device.
Thanks for the info. Any timeframe about when maestro will be able to run on real iOS device. Thanks