1 août 2023
Tests end to end avec Maestro
7 minutes de lecture
Dans l'ecosystème React Native, les outils d'instrumentation de tests end to end (E2E) sont très peu nombreux. En réalité jusqu'à il y a un an, on pouvait compter uniquement Detox et React-Native-Owl (ce dernier est utilisé à des fins de tests de régression notamment). En juillet 2022, un nouveau challenger est arrivé : Maestro. Partons aujourd'hui à la découverte de cet outil en testant une application réalisée à l'aide de React Native.
Maestro, c'est quoi ?
Tout comme Detox et Owl, Maestro est un outil d'instrumentation de tests. L'objectif est d'exécuter une suite d'instructions (clic, scroll, input, etc.) sur un appareil mobile (simulateur ou téléphone physique) et de vérifier que le résultat obtenu à l'écran est bien celui attendu. Contrairement à Detox et Owl, ces instructions seront écrites dans des fichiers YAML, là où les deux autres outils utilisent du JavaScript.
Installation
Maestro ne nécessite que l'installation d'un CLI. Nous allons ici l'installer pour Mac, mais il est également disponible pour Windows et Linux.
curl -Ls "https://get.maestro.mobile.dev" | bash
Et voilà, Maestro est installé. Nous allons maintenant créer notre application de test. Pour cela nous utiliserons Expo.
yarn create expo-app
Nous allons créer un simple formulaire d'authentification. À des fins de tests, nous allons gérer la validité de la combinaison identifiant / mot de passe de manière très simple, mais évidemment dans un cas concret cela serait géré par un serveur.
const login = (username: string, password: string) => {
return username === 'admin' && password === 'admin';
};
export default function App() {
const [success, setSuccess] = useState<boolean | undefined>();
const [username, setUsername] = useState<string>('');
const [password, setPassword] = useState<string>('');
const handleLogin = () => {
setSuccess(login(username, password));
};
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="Username"
onChangeText={setUsername}
autoCapitalize="none"
autoCorrect={false}
testID="username-input"
/>
<TextInput
style={styles.input}
placeholder="Password"
secureTextEntry
onChangeText={setPassword}
onSubmitEditing={handleLogin}
testID="password-input"
/>
<TouchableOpacity
style={styles.button}
activeOpacity={0.6}
onPress={handleLogin}
testID="login-button"
>
<Text style={{ color: 'black' }}>Connexion</Text>
</TouchableOpacity>
{success !== undefined && (
<Text style={{ color: success ? 'green' : 'red' }}>
{success ? 'Connexion réussie' : 'Connexion échouée'}
</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
justifyContent: 'center',
paddingHorizontal: 20,
gap: 16,
},
input: {
borderWidth: 1,
borderColor: 'lightgray',
borderRadius: 8,
height: 44,
paddingHorizontal: 16,
},
button: {
backgroundColor: 'skyblue',
height: 44,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
},
});
Ici un formulaire très simple : un champ texte pour l'identifiant et un autre pour le mot de passe ainsi qu'un bouton pour soumettre le formulaire. Un texte s'affiche en dessous pour indiquer si la connexion a réussi ou non.
Ecritures de tests
Nous allons maintenant écrire nos tests. Pour cela, nous allons créer un dossier e2e
à la racine du projet. Créons ensuite un fichier login.yaml
à l'intérieur de ce dossier. Ces fichiers sont, pour Maestro, des flows. Un flow est une suite de commandes à exécuter sur une application. Nous pouvons lui donner plusieurs attributs : un identifiant d'application, un nom, un tag, des variables d'environnement ainsi que des callbacks exécutés avant et après l'exécution du flow. Nous allons ici ajouter uniquement l'identifiant d'application, qui est obligatoire.
appId: com.maestro.test
---
Indiquons ensuite à Maestro d'ouvrir l'application.
- launchApp
Nous allons maintenant ajouter des instructions pour remplir le formulaire et le soumettre. Pour cela, nous allons utiliser les attributs testID
que nous avons ajouté au niveau des composants de notre application. Ils serviront de sélecteur pour Maestro.
- tapOn:
id: username-input
- inputText: admin
- pressKey: Enter
- tapOn:
id: password-input
- inputText: bad
- pressKey: Enter
Maintenant nous allons valider la présence du message de connexion échouée.
- assertVisible:
text: 'Connexion échouée'
Nous pouvons reproduire à la suite de ça les mêmes instructions pour tester une connexion réussie.
Néanmoins on remarque que cette répétition peut s'avérer exhaustive. Dans le cas d'une application plus importante, impliquant de nombreux champs ou bien tout simplement des étapes à répéter pour chaque flow comme par exemple un onboarding. Il serait intéressant de pouvoir regrouper ces instructions dans un fichier à part. Maestro nous permet de faire cela grâce à la commande runFlow. Créons un dossier subflows
et ajoutons-y à l'intérieur un fichier input.yaml
.
appId: com.maestro.test
---
- tapOn:
id: ${INPUT_ID}
- inputText: ${TEXT}
- pressKey: Enter
Ce flow devra donc recevoir deux variables d'environnement : INPUT_ID
et TEXT
. Il est important de conserver la configuration appId
afin d'indiquer l'identifiant d'application où les commandes doivent être exécutées. Nous pouvons maintenant l'utiliser dans notre flow principal.
appId: com.maestro.test
---
- launchApp
- runFlow:
file: subflows/input.yaml
env:
INPUT_ID: username-input
TEXT: admin
- runFlow:
file: subflows/input.yaml
env:
INPUT_ID: password-input
TEXT: bad
- assertVisible:
text: 'Connexion échouée'
- runFlow:
file: subflows/input.yaml
env:
INPUT_ID: password-input
TEXT: admin
- assertVisible:
text: 'Connexion réussie'
Nous avons ici écrit nos identifiants en dur dans les tests, mais il peut-être intéressant de les stocker à part, par exemple dans un fichier JavaScript où on peut ajouter de la logique. Dans notre cas, ce fichier sera très simple. Créons un dossier scripts
et ajoutons-y un fichier login.js
.
output.login = {
username: 'admin',
password: 'admin',
}
output
est une variable exposée par Maestro, que l'on peut par la suite utiliser dans notre test. Pour cela, nous devons d'abord exécuter le script via la commande runScript
- runScript: scripts/login.js
Nous pouvons maintenant utiliser nos différentes variables dans la suite de notre flow:
- runFlow:
file: subflows/input.yaml
env:
INPUT_ID: username-input
TEXT: ${output.login.username}
- runFlow:
file: subflows/input.yaml
env:
INPUT_ID: password-input
TEXT: bad
- assertVisible:
text: 'Connexion échouée'
- runFlow:
file: subflows/input.yaml
env:
INPUT_ID: password-input
TEXT: ${output.login.password}
- assertVisible:
text: 'Connexion réussie'
Il est maintenant temps d'exécuter notre test. Dans un premier temps, il sera nécessaire de produire un build en mode release de l'application, puis l'installer sur notre simulateur. Nous pouvons ensuite exécuter la commande suivante :
maestro test e2e/login.yaml
Si tout se passe bien, nos tests devraient s'afficher en vert.
Il est possible de lancer nos tests directement sur Expo Go. Pour cela, notre
appId
doit devenirhost.exp.Exponent
, la commandelaunchApp
devientopenLink: exp://127.0.0.1:19000
, enfin au moment du lancement des tests il faut s'assurer que notre bundler soit lancé viayarn start
.
Maestro Studio
Maestro propose une fonctionnalité appelée Maestro Studio. Il s'agit d'une interface web présentant une recopie de l'écran de notre simulateur afin de pouvoir y sélectionner des éléments et d'en obtenir les différentes commandes à utiliser dans nos flows. Cela peut s'avérer très pratique pour les applications complexes où il peut être difficile de trouver les identifiants des éléments à tester. On peut aussi y effectuer une suite de tests directement dans l'interface.
Pour exécuter Maestro Studio :
maestro studio
Vidéos de tests
Il est possible d'obtenir un enregistrement de nos tests, néanmoins ce n'est possible que pour un seul flow à la fois :
maestro record e2e/login.yaml
Le test va être exécuté, converti en vidéo et enregistré sur les serveurs de Maestro pour une durée limitée.
Intégration dans Github Actions
L'intérêt principal d'avoir des tests E2E est évidemment de pouvoir les exécuter dans le cadre d'une intégration continue. Maestro propose son propre service de test, Maestro Cloud, mais nous allons ici intégrer cela directement dans une CI Github Actions. Ici nous lancerons nos tests uniquement sur iOS, mais l'idéal est de les lancer sur toutes les plateformes couvertes par notre application.
name: iOS e2e maestro
on:
push:
branches: [main]
jobs:
test:
name: E2E test
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: yarn
- name: Install IDB
run: |
brew tap facebook/fb
brew install idb-companion
- name: Install dependencies
run: yarn
- name: Install Maestro
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
- name: Build & install app
run: |
npx expo prebuild -p ios
gem install xcpretty
yarn expo prebuild -p ios
xcodebuild -workspace ios/maestrotest.xcworkspace -configuration Release -sdk iphonesimulator -derivedDataPath ios/build -scheme maestrotest
open -a Simulator
sleep 10
xcrun simctl install booted ios/build/Build/Products/Release-iphonesimulator/maestrotest.app
- name: Run Maestro
run: maestro test e2e
env:
MAESTRO_DRIVER_STARTUP_TIMEOUT: 100000
- name: Upload artifacts
uses: actions/upload-artifact@v3
if: always()
with:
name: results
path: ~/.maestro/tests
Et voilà ! Vous pourrez trouver le résultat de tout ce que nous avons écrit sur ce repository.
Conclusion
Maestro est un outil de test E2E qui se veut bien plus simple d'utilisation que Detox, son principal concurrent, notamment de par sa simplicité de configuration. On notera notamment que Detox nécessite une configuration supplémentaire dans le code natif de notre application Android, ce qui n'est pas le cas de Maestro. L'écriture des tests en YAML peut être un frein au début notamment car nous pouvons souvent être en train de nous référer à la documentation lors de l'écriture de nos tests, néanmoins la présence de Studio simplifie grandement cela. Maestro bénéficie d'énormément de fonctionnalités que nous n'avons pas abordées ici, donc n'hésitez pas à consulter la documentation.
À vos tests !