Firebase Test Lab est une solution permettant d’opérer des tests sur des applications mobiles Android et iOS sur la plateforme Cloud préférée des développeurs mobiles : Firebase. Celle-ci propose des devices physiques ou virtuels afin de pouvoir tester une application sur une ou plusieurs configuration(s) de son choix.
Aujourd’hui la solution permet de lancer deux types de tests :
- Instrumentation ( Android )
- XCTest ( iOS )
Tout développeur Flutter aguerri sait qu’il n’est pas possible d’ajouter les tests d’intégration de Flutter aux différents tests pris en charge par Firebase Test Lab.
C’est ici qu’entre en jeu le tout dernier package développé par la team Flutter integration_test. Ce package a pour but de rendre compatible les tests développés avec flutter_test dans vos tests d’instrumentation.
Configuration du projet
La première chose à faire est d’ajouter la dépendance flutter_test si elle n’est pas déjà présente dans votre projet. C’est grâce à celle-ci que vous allez rédiger l’ensemble de vos tests :
# fichier pubspec.yaml
dev_dependencies:
flutter_test:
sdk: flutter
Il est important que les dépendances de test soient dans dev_dependencies
et non dependencies
afin de ne pas être incluses dans l’application.
À cette instant vous aurez accès à l’import package:flutter_test/flutter_test.dart
dans vos tests.
Puis ajoutez la dépendance à integration_test :
dev_dependencies:
# ...
integration_test: ^1.0.2+2
Au moment où je rédige cet article la dernière version up-to-date est la 1.0.2+2. N’hésitez-pas à adapter le numéro de version en conséquence.
Mise en place des tests
La mise en place des tests se passe en 2 étapes :
- Définition du driver entrypoint
- Écriture des premiers tests 😛
Driver entrypoint
Le driver entrypoint est une classe Dart qui va piloter le chargement des tests driver de votre application. Vous pouvez définir plusieurs drivers entrypoint dans une même application si nécessaire.
Un entrypoint se décrit de cette manière :
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();
Celui-ci peut être créé où vous le souhaitez. Pour la suite de cet article nous avons créé le répertoire test_driver
que vous devriez sans doute créer pour l’occasion. Puis nommer ce fichier integration_test.dart
avec le code présenté ci-dessus.
Écriture des premiers tests
Passons à la partie la plus importante le testing ! En Flutter il existe 3 types de tests :
- unit test
- widget test
- integration test
Pour réaliser nos tests d’intégrations avec la librairie integration_test , ce ne sont pas des tests d’intégration classiques que nous allons utiliser mais des widget test.
Les widget test sont plus communément utilisés pour tester une partie d’un écran, un widget, un groupe de widget. Leurs objectifs sont de vérifier le bon comportement d’un écran et que celui-ci réagit bien sous la contrainte du cycle de vie soumis par Flutter. Ce sont des tests effectués en boite noire, ils n’ont nul besoin d’un simulateur et s’apparente comme des tests unitaires pour vérifier le bon comportement d’une interface.
L’intérêt de la librairie integration_test est d’adapter les widget test pour les lancer sous forme de tests d’intégration supportés par le système natif, et aussi d’uniformiser vos widget test et integration test.
Pour faciliter le déroulement de l’article, les tests seront effectués sur le projet counter généré à la création d’un projet Flutter.
Pour rappel voici la commande pour créer un projet :
flutter create --org [votre package name] -i swift -a kotlin [nom de l'application]
Voici le StatefulWidget MyHomePage
auquel nous allons ajouter une Key
pour identifier plus facilement la valeur texte qui s’incrémente lors du clic :
class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Text( '$_counter', key: Key("counter_text"), // <-- add key style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); } }
Maintenant nous allons rédiger un cas de test simple, mais amplement suffisant pour cet article.
Créer le test suivant dans le répertoire integration_test/
et nommer le fichier counter_test.dart
:
import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:app/main.dart' as app; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets("test increment", (WidgetTester tester) async { app.main(); await tester.pumpAndSettle(); final fabFinder = find.byType(FloatingActionButton); expect(fabFinder, findsOneWidget); await tester.tap(fabFinder); // increment to 1 await tester.tap(fabFinder); // increment to 2 await tester.tap(fabFinder); // increment to 3 // await redraw await tester.pump(); final counterTextFinder = find.byKey(Key("counter_text")); expect(fabFinder, findsOneWidget); final Text text = tester.firstWidget(counterTextFinder); expect(text.data, "3"); }); }
Le test ci-dessus permet de vérifier que lorsqu’on clique 3 fois sur le bouton d’incrémentation (FloatingActionButton
) le label ayant comme clé Key("counter_text")
affiche bien la valeur 3 à l’écran.
À partir de ce moment, il est déjà possible de lancer les tests grâce à la commande :
flutter drive --driver=test_driver/integration_test.dart --target=integration_test/counter_test.dart
Si Flutter web est activé alors il sera possible d’executer les tests sur le web driver grâce à la commande suivante:
flutter drive --driver=test_driver/integration_test.dart --target=integration_test/counter_test.dart -d web-server
Lancer les tests d’instrumentation natif en local
Lancer les tests sur Android
Pour exécuter les tests sur Android en mode instrumentation, vous devrez obligatoirement avoir une classe de test qui chargera un runner dédié à cette tache. Cette classe est libre dans le nommage et devra être créée dans le répertoire /android/app/androidTest/java/votre package name /NomFlutterIntegrationTest.java.
Puis ajouter le code suivant :
package com.ineat.integration; import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import org.junit.Rule; import org.junit.runner.RunWith; @RunWith(FlutterTestRunner.class) public class IneatFlutterIntegrationTest { @Rule public ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class, true, false); }
N’oubliez-pas d’adapter le nom de la classe de test et le package name.
Dans cette classe vous trouverez @RunWith(FlutterTestRunner.class)
qui est un Runner développé spécifiquement dans la librairie integration_test
permettant de rendre compatible l’exécution les tests Flutter dans les tests d’intégration sous Android.
Puis, il est nécessaire d’ajouter les dépendances Espresso et Runner dans le fichier build.gradle
se situant dans le répertoire /android/app :
android { ... defaultConfig { ... testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } } dependencies { testImplementation 'junit:junit:4.12' // https://developer.android.com/jetpack/androidx/releases/test/#1.2.0 androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' }
Attention pensez à bien vérifier que votre projet est bien compatible androidX. Si ce n’est pas le cas il faudra alors monter de version.
À partir de ce moment il sera possible d’exécuter les tests d’instrumentation localement sur un simulateur ou un périphérique physique grâce à la commande :
./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../integration_test/counter_test.dart
( Commande à executer depuis le répertoire /android afin d’avoir accès à gradlew )
Lancer les tests sur iOS
Pour iOS, il suffit de se rendre dans le répertoire ios/
et de modifier le Podfile
en vérifiant que l’instruction use_framework
est bien activée :
target 'Runner' do use_frameworks! ... end
Puis pour exécuter les tests, il suffit de lancer la commande suivante à la racine du projet Flutter:
flutter build ios integration_test/counter_test.dart
Dans le répertoire ios/
, ouvrez le fichier Runner.xcodeproj
avec Xcode afin de créer un target de Test. Il suffit d’aller dans le menu de sélectionner File - New File - Target
puis d’ajouter ces lignes de code dans le fichier RunnerTests.m
créé pour l’occasion :
#import <XCTest/XCTest.h> #import <integration_test/IntegrationTestIosTest.h> INTEGRATION_TEST_IOS_RUNNER(RunnerTests)
Comme sur Android, ce fichier pourra avoir le nom que vous souhaitez. Donc n’hésitez-pas à le renommer.
Déploiement sur Firebase Test Lab
Les étapes consistent à préparer l’apk de l’application et l’application de test afin de pouvoir les uploader sur Firebase Test Lab. Il y a 3 commandes à lancer depuis le répertoire android/
:
flutter build apk
./gradlew app:assembleAndroidTest
./gradlew app:assembleDebug -Ptarget=`pwd`/../integration_test/counter_test.dart
Avant de pouvoir uploader des applications sur Firebase Test Lab, il est nécessaire d’avoir un compte de service. En effet, lorsqu’ on créé un projet sur la console Firebase, un compte de service est automatiquement créé comme on peut le voir ci-dessous :
Ce compte de service est administrateur sur tout le projet Firebase, il est donc pas approprié car il contient trop de permissions. Pour l’exploitation dans un outil tel une intégration continue, il est fortement recommandé de créer un compte de service avec les rôles minimum qui sont :
- Administrer Firebase Test Lab
- Ajouter des objets sur GCS ( Google Cloud Storage )
Pour créer un nouveau compte de service rendez-vous sur la console Google Cloud Platform, sélectionnez le bon projet, puis sélectionnez IAM/compte de service dans le menu latéral. Cette page permettra de lister l’ensemble des comptes et d’en créer de nouveaux avec les rôles adéquates.
Passons à la création de notre nouveau compte de service :
Comme annoncé précédemment ce compte de service aura besoin des rôles suivant :
- Administrateur de Firebase Test Lab pour déployer et lancer de nouveaux tests. Firebase Test Lab ne contenant que deux rôles : administrateur et lecture.
- Créateur des objets de l’espace de stockage afin de créer des objets dans un bucket
Une fois le compte de service créé, il suffira de générer la clé privée :
Puis de l’activer sur l’environnement cible grâce à la commande suivante :
gcloud auth activate-service-account --key-file=<CHEMIN VERS VOTRE CLÉ PRIVÉE>
Ensuite paramétrez votre CLI gcloud
avec le nom projet GCP. Celui-ci est visible depuis l’url de la console Firebase ou depuis la console Google Cloud Platform :
gcloud --quiet config set project <NOM DU PROJET>
Allez on se jette à l’eau ? il est temps de déployer et lancer les tests sur Firebase Test Lab. Tout est configuré il ne reste plus qu’à exécuter :
gcloud firebase test android run \ --type instrumentation \ --app build/app/outputs/apk/debug/app-debug.apk \ --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk \ --timeout 2m \ --results-bucket=flutter_tips_8
( Adapter les options –results et timeout qui permettent de choisir le nom du dossier dans le bucket et la durée du timeout )
Si tout se passe bien, vous devriez avoir la sortie de console suivante :
Uploading [build/app/outputs/apk/debug/app-debug.apk] to Firebase Test Lab... Uploading [build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk] to Firebase Test Lab... Raw results will be stored in your GCS bucket at [https://console.developers.google.com/storage/browser/flutter_tips_8/2021-02-19_15:27:28.XXXX/] Test [matrix-dk4musstnm2ia] has been created in the Google Cloud. Firebase Test Lab will execute your instrumentation test on 1 device(s). Creating individual test executions...done. Test results will be streamed to [https://console.firebase.google.com/project/VOTRE_PROJET_FIREBASE/testlab/histories/bh.XXXXXXXXX/matrices/XXXXXXXXXXXXX]. 15:28:07 Test is Pending 15:29:27 Starting attempt 1. 15:29:27 Started logcat recording. 15:29:27 Started crash monitoring. 15:29:27 Preparing device. 15:29:27 Test is Running 15:29:34 Logging in to Google account on device. 15:29:34 Installing apps. 15:29:47 Retrieving Pre-Test Package Stats information from the device. 15:29:47 Retrieving Performance Environment information from the device. 15:29:47 Started crash detection. 15:29:47 Started Out of memory detection 15:29:47 Started performance monitoring. 15:29:53 Started video recording. 15:29:53 Starting instrumentation test. 15:30:00 Completed instrumentation test. 15:30:07 Stopped performance monitoring. 15:30:07 Retrieving Post-test Package Stats information from the device. 15:30:13 Logging out of Google account on device. 15:30:13 Stopped crash monitoring. 15:30:13 Stopped logcat recording. 15:30:13 Done. Test time = 7 (secs) 15:30:13 Starting results processing. Attempt: 1 15:30:13 Completed results processing. Time taken = 3 (secs) 15:30:13 Test is Finished Instrumentation testing complete.
On peut constater que pour chaque job de test effectué, Firebase Test Lab créé un répertoire associé dans GCS. C’est pour cela qu’il est nécessaire d’ajouter des droits d’écriture GCS à votre compte de service.
Chaque répertoire contiendra les applications, une vidéo des tests, la sortie console, et les résultats au format XML :
Il est également possible de consulter la liste de vos jobs depuis la console Firebase :
Et d’avoir le détail pour chacun d’entre eux :
Maintenant vous avez toutes les billes mettre en place Firebase Test Lab sur votre projet Flutter.
Vous souhaitez mettre en place Firebase Test Lab sur Codemagic ?
Alors sachez qu’ils ont rédigé un article qui pourra être un bon complément pour vous aider. D’ici le prochain épisode testez bien vos applications 😜
Série Flutter of the month
- Tips #1 : Personnaliser la shape d’une BottomSheet
- Tips #2 : Hero – le super widget
- Tips #3 : Responsive widgets
- Tips #4 : Codemagic déployer vers Firebase App Distribution
- Tips #5 : Exploiter votre code coverage avec Codecov.io
- Tips #6 : Embarquez à bord du Zeplin, destination Flutter
- Tips #7 : Merry Christmas
- Tips #8 : Firebase Test Lab