Mobile app testing easier with Maestro

Languages

View full on my Github: https://github.com/tuantvk/rnmaestro

I used Detox to test React Native apps. At that time, Detox was so “cool”, saving both the time and effort of the dev team and the tester team. However, later on, I saw the complexity, as well as the “difficulty” with new team members, and that’s when Maestro came to me as a savior. I know Maestro through an article on dev.to, but my team’s “noob” is true 😂 (but my teammates don’t believe it).

Table of Contents

  • What is Maestro?
  • Setting up the development environment and initializing the app
  • Installing Maestro
  • Test flows
  • Maestro studio
  • Test case
  • Interaction with a component by testID
  • External parameters
  • runFlow
  • Recording your flow
  • Tags
  • Maestro Cloud
  • Videos testing of Maestro

What is Maestro?

Maestro is the simplest and most effective mobile UI testing framework. Maestro is built on learnings from its predecessors (Appium, Espresso, UIAutomator, XCTest) and allows you to easily define and test your Flows.

What are Flows? Think of Flows as parts of the user journey in your app. Login, Checkout and Add to Cart are three examples of possible Flows that can be defined and tested using Maestro. Declarative yet powerful syntax and write your tests in a yaml or yml file. Read more Why Maestro?

Platform Support:

PlatformSupport
iOS
Android
React Native
Flutter
Web Views

I like using Maestro for mobile app testing. Installing and writing tests is also very easy for all those who do not know how to use the Casio FX-570 calculator.
In this example, I will guide you to install and write some common test cases. I use Mac OS and a simple app written in React Native.

Setting up the development environment and initializing the app

First, you must have the application to be tested. To create a React Native project, you can refer to the full steps in Setting up the development environment.

Assuming you already have the environment, proceed with initializing the application:

npx react-native init RNMaestro

After initialization, notice the applicationId of Android (android/app/build.gradle -> applicationId) and iOS (Project in Xcode -> Signing & Capabilities -> Bundle Identifier). You can optionally edit them for later use, in this tutorial, I edit Android and iOS to com.rnmaestro.

In the App.tsx file of the project, copy & paste the code below.

// App.tsx
import React, { useState } from 'react';
import {
  View,
  Alert,
  SafeAreaView,
  TextInput,
  Button,
  FlatList,
} from 'react-native';

const TASKS = Array.from({ length: 25 }, (_, i) => ({ title: 'Task ' + i }));

interface Item {
  title: string;
}

const App = () => {
  const [title, setTitle] = useState('');
  const [tasks, setTasks] = useState(TASKS);

  const addTask = () => {
    if (title?.trim()?.length === 0) {
      Alert.alert('Title is required');
    } else {
      const newTasks = [...tasks];
      newTasks.unshift({ title });
      setTasks(newTasks);
      setTitle('');
    }
  };

  const renderItem = ({ item }: { item: Item }) => (
    <Button title={item?.title} onPress={() => Alert.alert(item?.title)} />
  );

  const keyExtractor = (item: Item, idx: number) => `${idx}`;

  return (
    <SafeAreaView>
      <FlatList
        data={tasks}
        renderItem={renderItem}
        keyExtractor={keyExtractor}
        ListHeaderComponent={
          <View>
            <TextInput
              value={title}
              placeholder="Enter your title"
              onChangeText={setTitle}
            />
            <Button testID="btn_add_task" title="Add task" onPress={addTask} />
          </View>
        }
      />
    </SafeAreaView>
  );
};

export default App;

Or you can use my project (skip the step below if you don’t use it):

  • Clone repository
git clone https://github.com/tuantvk/rnmaestro.git
  • Installing packages
cd rnmaestro; yarn install
  • Pod (Only for iOS)
npx pod-install

Installing Maestro

For installation information for Windows or other environments, please refer to the official documentation Installing Maestro .

Run the following command to install Maestro on Mac OS, Linux:

curl -Ls "https://get.maestro.mobile.dev" | bash

You can check if maestro is installed by checking the version:

maestro -v

It should print the version number vi.xxx.com (E.g: 1.27.0).
In case zsh: command not found: maestro, restart your terminal please.

Running flows on iOS Simulator requires installation of Facebook IDB:

brew tap facebook/fb
brew install idb-companion
  • Xcode recommended version is 14 or higher.
  • Maestro can’t interact with real iOS devices yet. Only Simulator is supported at the moment. (May 2023)

After completing the above steps, the installation is complete. Start writing test cases.

https://media1.giphy.com/media/26u4lOMA8JKSnL9Uk/giphy.gif

Test flows

Based on the functionality of the current application, our workflow should look like this:

  1. Start the app
  2. Press Add task button
  3. Check if the empty message is visible
  4. Enter title
  5. Press Add task button
  6. Check if the new task is visible

Maestro studio

Use Maestro Studio to instantly discover the exact commands needed to interact with your app.

maestro studio

Run the command above to launch Maestro Studio in your browser, default is http://localhost:9999.

Video demo on Github – Maestro Studio

Test case

Create file .maestro/app.yaml in the root folder of the project.

# .maestro/app.yaml
appId: com.rnmaestro # applicationId
---
- launchApp
# Check if "Title is required" is visible
- tapOn: "Add task"
- assertVisible: "Title is required"
- tapOn: "OK"

# Check if new task is visible
- tapOn: "Enter your title"
- inputText: "Task from maestro"
- hideKeyboard # Note 1
- tapOn: "Add task"
- assertVisible: "Task from maestro"

Note 1:
On iOS, hideKeyboard is done with help of scrolling up and down from the middle of the screen since there is no native API to hide the keyboard. If using this command doesn’t hide the keyboard we recommend clicking on some non-tappable region with tapOn points command, similarly to how a user would hide the keyboard when interacting with your app. Read more iOS implementation caveat.

Run the associated Flow using the maestro test command.

# run single flow
maestro test .maestro/app.yaml
# or
# run all flows in a directory
maestro test .maestro/

View demo on Github – Test case

In terminal looks similar to the image below:

The test will be automatically restarted whenever you make a change to the test file. This is particularly convenient when writing a new test from ground up. Run with -c argument.

maestro test -c .maestro/app.yaml

Commands

assertVisibleassertNotVisibleassertTrueback
clearKeychainclearStatecopyTextFromevalScript
eraseTextextendedWaitUntilhideKeyboardinputText
launchAppopenLinkpressKeypasteText
repeatrunFlowrunScriptscroll
scrollUntilVisiblesetLocationstopAppswipe
takeScreenshottapOntravelwaitForAnimationToEnd

Read more Maestro – Commands.

Interaction with a component by testID

In the example above, I was instructed to write the flow by calling the contents of the screen directly. However, there will be many testing parts whose content changes after each operation, so you need to use testID to identify like: View, Button, Text, Image.

Example:

# .maestro/app.yaml
appId: com.rnmaestro # applicationId
---
- launchApp
# Check if "Title is required" is visible
- tapOn:
    id: "btn_add_task" # testID here
- assertVisible: "Title is required"
- tapOn: "OK"

External parameters

There might be cases where you don’t want to store certain values in a test file itself, you can pass parameters to Maestro:

maestro test -e APP_ID=com.rnmaestro .maestro/app.yaml

And then refer to them in your flow using ${name} notation:

# .maestro/app.yaml
appId: ${APP_ID} # applicationId
---
- launchApp

Constants can be declared at the flow file level, in key env, above the --- marker:

# .maestro/app.yaml
appId: ${APP_ID} # applicationId
env:
  APP_ID: com.rnmaestro
---
- launchApp

If you want to run tests from scripts of package.json you can config:

{
  "scripts": {
    "test": "$HOME/.maestro/bin/maestro test",
    "test-dev": "yarn test -e APP_ID=com.rnmaestro.dev",
    "test-prod": "yarn test -e APP_ID=com.rnmaestro"
  }
}

In case, I have 2 environments dev and production.

  • com.rnmaestro.dev is for dev environments
  • com.rnmaestro is for production environments

Run test:

yarn run test-prod .maestro/app.yaml

runFlow

If you’d like to avoid duplication of code or otherwise modularize your Flow files, you can use the runFlow command to run commands from another file. Example:

# Login.yaml
appId: com.example.app
---
- launchApp
- tapOn: Username
- inputText: Test User
- tapOn: Password
- inputText: Test Password
- tapOn: Login
# Settings.yaml
appId: com.example.app
---
- runFlow: Login.yaml # Run commands from `Login.yaml`
- tapOn: Settings
- assertVisible: Switch to dark mode

Read more Maestro – runFlow.

Recording your flow

Simply run the command below:

maestro record .maestro/app.yaml

After testing is complete, maestro renders a beautiful mp4 video recording of the entire process.

Currently, Maestro versions CLI 1.26.0, CLI 1.26.1, CLI 1.27.0, the record feature does not work on iOS, but it has been fixed at commit 2bd380d, but no release yet. If you are using the above versions, it is possible that the screen recording feature will not work (Updated date: 2023-05-09).

Tags

There is a couple of different use cases for this, but this is especially useful when you want to run some Flows at Pull Request time, and other Flows before a version release. The --include-tags will look for all flows containing the provided tag; it doesn’t matter if those Flows also have other tags. On the other hand, the --exclude-tags parameter will remove from the list of Flows run any Flow that contains the provided tags. Example:

# flowA.yaml
appId: com.example.app
tags: 
  - dev
  - pull-request
# flowB.yaml
appId: com.example.app
tags: 
  - dev
maestro test --include-tags=dev --exclude-tags=pull-request workspaceFolder/

In the scenario above:

  • If they use --include-tags=dev, flowA and flowB will run.
  • If they use --include-tags=dev,pull-request, both flows will run.
  • If they use --exclude-tags=pull-request, only flowB will run.
  • If they use --exclude-tags=dev none Flow will run.
  • If they use --include-tags=dev --exclude-tags=pull-request, only flowB will run.

Read more Maestro – Tags.

Maestro Cloud

The easiest way to test your Flows in CI is to run your Flows on Maestro Cloud. Since your flows run in the cloud there’s no need to configure any simulators or emulators on your end. Check out the Maestro Cloud Documentation.

CI Support:

CI PlatformSupport via CLINative Integration
GitHub Actions
Bitrise
Bitbucket
CircleCI
GitLab CI/CD🚧
TravisCI
Jenkins
All other CI platforms

Videos testing of Maestro

Android contacts flow automation
Facebook signup flow automation

Resources

https://media.tenor.com/blHCE4Hrc20AAAAd/bravo.gif

Maestro is also very new to the mobile application testing community, there are many issues to fix and upgrade. However, it is well deserved 1 star on the Maestro Github for the development team.

🎉 🎉 🎉 Hope the article is useful to everyone! Thanks! 🎉 🎉 🎉

Contributions

Any comments and suggestions are always welcome. Please make Issues or Pull requests for me.