🚀 Getting Started with Maestro UI for Mobile UI Testing

Table of contents
UI testing is a crucial part of any mobile app development workflow. Whether you're building with React Native, Flutter, Swift, or Kotlin, ensuring that your app's user interface behaves as expected is key to a smooth user experience.
In this article, we’ll explore how to use Maestro — a simple and powerful tool for automating UI tests for mobile apps.
Maestro is a UI testing framework specifically built for mobile apps. It's:
Easy to set up
Uses YAML files for test scripts
Supports both iOS and Android
Great for end-to-end (E2E) testing
Think of it as a simpler, lighter alternative to Appium or Detox — ideal for fast iterations and CI/CD pipelines.
Installation
I will guide you through how to install Maestro for different operating systems.
macOS
If you use Homebrew, install Maestro with:
"brew install mobile-dev-inc/tap/maestro"
Once installed, verify it:
"maestro --version"
Make sure you have Java installed (
brew install openjdk
), as Maestro requires Java 11+ to run.
Windows
Install Java
Download Maestro CLI
Go to the official Maestro Releases Page
Download the latest
.zip
file for WindowsExtract it to a folder (e.g.,
C:\maestro
)Add that folder to your Environment Variables → PATH
Verify Installation:
"maestro --version"
Linux
- Install Java 11+
"sudo apt update"
"sudo apt install openjdk-11-jdk"
- Download Maestro
"curl -Ls "https://get.maestro.mobile.dev" | bash"
- Move to
/usr/local/bin
for global access:
"sudo mv maestro /usr/local/bin/maestro"
- Check it works:
"maestro --version"
Using Maestro in your code
Create a folder in your project for your test flows or on your terminal you can run:
"mkdir maestro-tests && cd maestro-tests"
In your component or screen that you would like to test, you have to add test ids to various buttons you want to be identified
Example (Login.tsx) in react native.
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native';
const Login = ({ navigation }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleLogin = () => {
if (email === 'user@example.com' && password === 'password123') {
Alert.alert('Success', 'Welcome');
} else {
Alert.alert('Error', 'Invalid credentials');
}
};
return (
<View style={styles.container}>
<Text style={styles.title} testID="login-title">Login</Text>
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
testID="email-input"
/>
<TextInput
style={styles.input}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
testID="password-input"
/>
<TouchableOpacity style={styles.button} onPress={handleLogin} testID="submit-button">
<Text style={styles.buttonText}>Submit</Text>
</TouchableOpacity>
</View>
);
};
export default Login;
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
backgroundColor: '#fff',
},
title: {
fontSize: 28,
marginBottom: 24,
textAlign: 'center',
fontWeight: 'bold',
},
input: {
height: 50,
borderWidth: 1,
borderColor: '#ddd',
paddingHorizontal: 12,
marginBottom: 16,
borderRadius: 8,
},
button: {
backgroundColor: '#007bff',
paddingVertical: 14,
borderRadius: 8,
},
buttonText: {
color: '#fff',
textAlign: 'center',
fontSize: 16,
},
});
Since everything is up and running we can start testing, so let’s write our first flow. We are going to write commands to launch the app.
Step 1: Create a new file with login**.yaml** extension inside of the maestro-tests folder we initially created.
Steps 2: In your login.yaml file
appId: com.example.myapp
---
- launchApp
- tapOn: "email-input"
- inputText: "user@example.com"
- tapOn: "password-input"
- inputText: "password123"
- tapOn: "submit-button"
- assertVisible: "Welcome"
On your terminal run the command to carry out your test:
maestro test login_flow.yaml
Congratulation! You have successfully created a test case for your login screen.
Here are some other available Maestro commands you might find help.
- launchApp: # Launches the app under test
- stopApp: # Stops current application
- tapOn: # Taps on a selected element
- doubleTapOn: # Double taps on a selected element
- inputText: # Inputs text
- eraseText: # Removes characters from the currently selected text field
- assertVisible: # Asserts whether an element is visible
- assertNotVisible: # Asserts whether an element is not visible
- copyTextFrom: # Copies text from an element and saves it in-memory
- pasteText: # Pastes any text copied with copyTextFrom into the currently focused field
- startRecording: # Starts a screen recording
- stopRecording: # Stops a running screen recording
- scroll: # Does a vertical scroll
- scrollUntilVisible: # Scrolls towards a direction until an element becomes visible in the view hierarchy
- waitForAnimationToEnd: # Waits until an ongoing animation/video is fully finished, and the screen becomes static
- extendedWaitUntil: # Waits until an element becomes visible within a specified amount of time
- back: # Navigates the user to the previous screen
- pressKey: # Presses a set of special keys
- runFlow: # Runs commands from another file
- runScript: # Runs a provided JavaScript file
- setLocation: # Applies a mock geolocation to the device
- travel: # Mocks the motion of the user, by specifying a set of points and a speed
- clearState: # Clears the application state
- clearKeychain: # Clears the entire iOS keychain
- hideKeyboard: # Hides the software keyboard
- swipe: # Makes the swipe gesture
- addMedia: # Adds media to the device’s gallery
- takeScreenshot: # Saves a screenshot in a PNG file
- assertTrue: # Asserts whether the given value is either true or non-empty
- repeat: # Repeats set of commands N times
- openLink: # Opens a link on a device
- evalScript: # Allows specifying JavaScript directly in the Maestro flow
Subscribe to my newsletter
Read articles from Matthias Ehizojie directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
