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