Tester son app React Native - End-to-End avec Detox

7 minutes de lecture

Tester son app React Native - End-to-End avec Detox

Si vous regardez tout en haut de la pyramide, vous apercevrez les tests end-to-end (E2E). Au dessus du typing, des tests unitaires et fonctionnels, ils permettent de tester votre application comme un utilisateur le ferait réellement. Ils ont l’avantage de s’émanciper des détails d’implémentation. Nous avions vu comment les mettre en place sur un navigateur grâce à Cypress. Mais qu’en est-il sur un simulateur iOS ou Android ?

La réponse : Detox, un framework de tests développé par Wix (connu pour être actif dans la communauté React). L’un de ses principaux avantages est de gérer automatiquement les attentes entre les instructions de tests. Lors d’appels asynchrones (requêtes, animations, timers…), Detox attend automatiquement la fin d’exécution avant de passer à l’instruction suivante.

Pourquoi des tests E2E ?

Il existe plusieurs types de tests (unitaires, fonctionnels, E2E), le deux premiers types peuvent être difficiles à mettre en place car il faut se soucier de l’implémentation, c’est pourquoi il est important de les écrire au fil de l’eau. Les tests end-to-end, quant à eux, peuvent facilement être mis en place en fin de développement. Ils sont cependant les plus coûteux en terme de ressources (simulateur iOS / Android) et en temps d’exécution.

Installer Detox

Installer les outils et le CLI

Nous utilisons Homebrew pour installer applesimutils (une collection d’utilitaires pour piloter les simulateurs iOS) :

brew tap wix/brew
brew install applesimutils

Puis le CLI Detox via Yarn :

yarn global add detox-cli

Ajouter Detox à son projet React Native

Installez la librairie Detox en dépendance de développement de votre projet :

yarn add --dev detox

Configurez Detox

Il faut maintenant indiquer sur quel simulateur et avec quel build vous voulez exécuter vos tests. Pour cela, ajoutez une section detox à votre package.json :

{
  ...
  "detox": {
    "configurations": {
      "ios.sim.debug": {
        "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/app.app",
        "build": "xcodebuild -workspace ios/app.xcworkspace -configuration Debug -sdk iphonesimulator -scheme app -derivedDataPath ios/build",
        "type": "ios.simulator≤",
        "name": "iPhone X"
      }
    }
  }
}

Nous créons une configuration ios.sim.debug (le choix du nom est libre) décrivant l’emplacement du binaire (binaryPath), la commande de build, le type (ios.simulator) ainsi que le nom du simulateur (dans notre cas iPhone X). Vous pouvez ajouter d’autres configurations (par exemple en mode Release ou sur un simulateur Android).

Initialiser vos tests

Le CLI Detox propose une commande pour créer la structure et la configuration de vos tests :

$ detox init -r jest

detox[986] INFO:  [init.js] Created a file at path: e2e/config.json
detox[986] INFO:  [init.js] Created a file at path: e2e/init.js
detox[986] INFO:  [init.js] Created a file at path: e2e/firstTest.spec.js
detox[986] INFO:  [init.js] Patching package.json
detox[986] INFO:  [init.js]   json["detox"]["test-runner"] = "jest";

Celle-ci va ajouter un dossier e2e à la racine de votre projet et mettre à jour votre configuration dans le package.json avec le test runner voulu (Jest dans notre cas). Le dossier contient la config minimale et un exemple dévoilant la structure d’un test :

describe('Example', () => {
  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should have welcome screen', async () => {
    await expect(element(by.id('welcome'))).toBeVisible();
  });

  it('should show hello screen after tap', async () => {
    await element(by.id('hello_button')).tap();
    await expect(element(by.text('Hello!!!'))).toBeVisible();
  });

  it('should show world screen after tap', async () => {
    await element(by.id('world_button')).tap();
    await expect(element(by.text('World!!!'))).toBeVisible();
  });
});
Detox est compatible avec Jest et Mocha. Nous utilisons Jest qui est installé par défaut avec React Native (et que je préfère personnellement)

Vous pouvez maintenant compiler et lancer vos tests :

detox build
detox test

Si vous avez renseigné plusieurs configurations, il vous faut passer l’option --configuration avec son nom :

detox build --configuration ios.sim.debug
detox test --configuration ios.sim.debug

Il est maintenant temps d’écrire vos tests !

Écrire ses tests

Les tests tournent autour de 3 concepts :

Les actions
📒 Documentation

Les actions permettent d’interagir avec les élements de votre application (boutons, vues…), elles permettent de simuler les principales interactions sur mobile :

  • tap()
  • longPress()
  • multiTap()
  • scroll()
  • scrollTo()
  • swipe()

Les sélecteurs (Matchers)
📒 Documentation

Les sélecteurs permettent de sélectionner des élements de votre application :

  • by.id()
  • by.text()
  • by.label()

by.id() sélectionne un élement disposant d’un attribut testID :

<Text testID="name">Baptiste</Text>

Utiliser les attributs testID est une bonne pratique car ils découplent les tests de la logique applicative. C’est pourquoi les composants React Native supportent cet attribut.

Les validateurs (Expectations)
📒 Documentation

Les validateurs permettent de valider l’existence d’un élement :

  • toBeVisible()
  • toBeNotVisible()
  • toHaveText()
  • scroll()
  • toHaveId()

Tester un écran d’onboarding

Nous allons tester un écran d’onboarding, mis en place grâce au composant react-native-onboarding-swiper. Il permet de créer rapidement une suite d’écrans introduisant l’application lors du premier lancement :

react-native-onboarding-swiper

Voici notre composant :

const OnboardingScreen = () => (
  <View testID="scrollView" style={{ flex: 1 }}>
    <Onboarding
      pages={[
        {
          backgroundColor: '#7ed6df',
          title: <Text testID="hello">Hello 👋</Text>,
          subtitle: 'Find ticket',
        },
        {
          backgroundColor: '#f6e58d',
          title: <Text testID="fast">Fast 🐎</Text>,
          subtitle: 'Buy ticket',
        },
        {
          backgroundColor: '#fab1a0',
          title: <Text testID="secure">Secure 👀</Text>,
          subtitle: 'Encrypted communication',
        },
      ]}
    />
  </View>
);

Nous allons utiliser l’action swipe pour naviguer entre nos écrans ainsi que l’action tap pour cliquer sur le bouton Next. Enfin nous testons que chaque composant Text est bien visible :

describe('Onboarding', () => {
  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should have onboarding screen', async () => {
    const view = element(by.id('scrollView'));

    await expect(element(by.id('hello'))).toBeVisible();

    await view.swipe('left');
    await expect(element(by.id('fast'))).toBeVisible();

    await view.swipe('right');
    await expect(element(by.id('hello'))).toBeVisible();

    await view.swipe('left');
    await view.swipe('left');
    await expect(element(by.id('secure'))).toBeVisible();

    await view.swipe('right');
    await expect(element(by.id('fast'))).toBeVisible();

    await element(by.text('Next')).tap();
    await expect(element(by.id('secure'))).toBeVisible();
  });
});

Nous pouvons maintenant lancer nos tests avec la commande detox test :

$ detox test
Example: should have onboarding screen [OK]
 PASS  e2e/onboarding.spec.js (19.783s)
  Example
    ✓ should have onboarding screen (12146ms)

Les tests passent ✅

Et les mocks ?

Comment faire si vous voulez mocker un appel à une API tiers dans vos tests ? Avec Detox, contrairement à Jest où vous pouvez définir spécifiquement des mocks au runtime, les mocks se font lors de la compilation de votre app. La méthode est de renseigner un fichier spécifique qui va venir remplacer l’original. Ces fichiers doivent être suffixés par .e2e.js. Prenons l’exemple d’un appel vers une API lors de l’appui sur un bouton :

import api from './client/api';

const Home = () => {
  const [movies, setMovies] = useState([]);

  <View>
    <Button
      title="Get movies"
      onPress={async () => {
        const data = await api.getMovies();
        setMovies(data);
      }}
    />
  </View>;
};

Pour mocker cet appel avec Detox, il vous faudra créer un fichier api.e2e.js au même niveau que votre fichier api.js :

> src
  Home.js
  > client
    api.js
    api.e2e.js

Lors du build, Detox chargera non pas le fichier api.js mais le fichier api.e2e.js. Ce fichier peut retourner directement une payload json sans faire d’appel à votre API. Pour mettre en place ce mécanisme, il vous faudra tout de même suivre ces quelques étapes.

Aller plus loin

Nous avons vu comment utiliser Detox pour mettre en place des tests E2E sur React Native. Detox propose d’autres fonctionnalités comme la génération de screenshots ou de vidéos. Enfin, une suite de tests n’est utile que si elle ne tourne sur un serveur d’intégration continue. La documentation explique comment faire tourner vos tests sur Travis ou Bitrise, vous pouvez la retrouver ici.

Faites chauffer les simulateurs ! 🔥

👋

Continuer la discussion sur Twitter