DEV Community

Alexander Hodes for B42

Posted on

Test your React Native app with Maestro

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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',
  },
});
Enter fullscreen mode Exit fullscreen mode

Screenshot Sign In

Screenshot Sign In Success

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
Enter fullscreen mode Exit fullscreen mode

Running flows on iOS Simulator requires installation of Facebook IDB.

$ brew tap facebook/fb
$ brew install facebook/fb/idb-companion
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Creating test flows

Our workflow should look like this:

  1. Start the app
  2. Enter username
  3. Enter password
  4. Press sign in button
  5. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Maestro studio

The easiest one looks like this:

# tap on username
- tapOn:
    text: "Username"
    index: 1
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Sign In Workflow

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Maestro Record

Below you can find an example recording.

Maestro 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"
Enter fullscreen mode Exit fullscreen mode

You can run this workflow like this

$ maestro test -e USERNAME="Test User" -e PASSWORD=Test123456 .maestro/sign-in-flow-external-parameters.yaml
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

This workflow can be started like any other flow without any external parameters.

$ maestro test .maestro/sign-in-flow-constants.yaml
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Further resources

Top comments (11)

Collapse
 
retyui profile image
Davyd NRB

That is untreatable! Definitely, I want to move away from Appium.

Do you know how we can launch app with custom arguments?

// Appium api (see: http://appium.io/docs/en/writing-running-appium/caps/)
capabilities: {
  optionalIntentArguments: `--ez myBool true --es myStr 'string text'`, // Android
  processArguments: {args : ['-myBool', 'true','-myStr', 'string text']}, // iOS
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
alexanderhodes profile image
Alexander Hodes

Sounds great, I can really recommend Maestro.
You can use the parameters from maestro, e.g.

maestro test -e myBool=true -e myStr =string .maestro/flow.yaml
Enter fullscreen mode Exit fullscreen mode

You can access those external parameters like this in your workflow file:

${USERNAME}
Enter fullscreen mode Exit fullscreen mode

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...

Collapse
 
retyui profile image
Davyd NRB

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

Collapse
 
jakecarpenter profile image
Jake Carpenter

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?

Collapse
 
alexanderhodes profile image
Alexander Hodes

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...

Collapse
 
tiaeastwood profile image
Tia Eastwood

Thanks for this, I've just started using maestro and this has some useful examples

Collapse
 
retyui profile image
Davyd NRB

Another cool feature that have to mentioned it is conditions

- runFlow:
    when:
      visible: Password is required
    commands:
      - tapOn: "Enter password"
      - inputText: "my password"

# or 

- runFlow:
    env:
      true: ${IS_IOS == 'true'}
    commands:
      - tapOn: Delete Account
Enter fullscreen mode Exit fullscreen mode
Collapse
 
retyui profile image
Davyd NRB • Edited

Will be great to add that all MAESTRO_* env variabled will be available in flow without passing them as -e MY_VAR=myVal

export MAESTRO_USERNAME=123

maestro test .maestro/flow.yaml
Enter fullscreen mode Exit fullscreen mode

and then

${MAESTRO_USERNAME} # 123

Collapse
 
tilak profile image
Tilak Vattikuti

How to run on my local real devices (actual mobile devices) instead of virtual devices

Collapse
 
vasylnahuliak profile image
Vasyl Nahuliak

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.

Collapse
 
jay_kahn_yz profile image
Jay Kahn

Thanks for the info. Any timeframe about when maestro will be able to run on real iOS device. Thanks