Retour au blog
    23 août 2026Tutorial

    Votre suite mobile n'est pas lente, elle est séquentielle

    Une suite Appium de 400 tests qui prend 90 minutes, ce n'est généralement pas 90 minutes de tests lents. C'est 90 minutes pendant lesquelles un seul appareil fait 400 choses à la suite.

    Vous pouvez grappiller des secondes sur chaque étape indéfiniment sans jamais corriger cela. Le seul levier qui ait un vrai effet, c'est le nombre d'appareils qui travaillent en même temps — et les raisons pour lesquelles les équipes ne l'actionnent pas ne sont presque jamais techniques.

    L'arithmétique que personne ne fait

    Prenez la suite que vous avez. Le temps total d'exécution vaut à peu près :

    wall_clock ≈ (sum of all test durations / devices) + slowest_single_test + setup_per_device

    Deux conséquences en découlent immédiatement.

    Un : passé un certain point, ajouter des appareils n'aide plus. Si votre test le plus long dure 6 minutes, aucune dose de parallélisme ne fera passer la suite sous les 6 minutes. Découper ce seul test vaut plus que les quatre appareils suivants.

    Deux : setup_per_device se paie une fois par partition, pas une fois par suite. Si chaque partition installe l'application, attend qu'un appareil lui soit attribué, se connecte et initialise ses données, alors un découpage en 12 paie tout cela douze fois. Les équipes passent couramment de 4 à 12 partitions, constatent que le temps total bouge à peine, et en concluent que le parallélisme ne marche pas sur mobile. Ce qui s'est réellement passé, c'est que la préparation est passée de 20 % à 60 % de l'exécution.

    Mesurez ces deux nombres avant d'acheter quoi que ce soit.

    Où couper la suite

    Partitionner par « fichier de test, par ordre alphabétique, en N paquets » est le réglage par défaut de la plupart des modèles de CI, et c'est le pire disponible. Sur mobile, les durées de test sont extrêmement inégales — un test de connexion prend 20 secondes, un test de commande jusqu'au paiement en prend quatre minutes — si bien que des paquets alphabétiques vous donnent une partition qui finit en trois minutes et une autre qui tourne pendant vingt. Votre suite est aussi lente que cette dernière partition.

    Deux meilleures options, par ordre d'effort croissant :

    Équilibrer par durée mesurée. Conservez les temps par test de la dernière exécution dans un fichier JSON du dépôt, triez les tests par ordre décroissant, et affectez chacun à la partition qui a pour l'instant le moins de travail. Quinze lignes de code, et cela bat typiquement le découpage alphabétique de 30 à 40 % sur le temps total :

    js
    // shard.mjs — longest-first bin packing
    const timings = JSON.parse(fs.readFileSync("test-timings.json", "utf8"));
    const tests = allTests.sort((a, b) => (timings[b] ?? 60) - (timings[a] ?? 60));
    
    const shards = Array.from({ length: N }, () => ({ total: 0, tests: [] }));
    for (const test of tests) {
      const lightest = shards.reduce((a, b) => (a.total <= b.total ? a : b));
      lightest.tests.push(test);
      lightest.total += timings[test] ?? 60;
    }

    Les tests inconnus se voient attribuer une estimation moyenne, de sorte qu'un nouveau test n'atterrit jamais dans une partition déjà pleine.

    Découper plutôt par contrainte matérielle. Certains tests n'ont de sens que sur un appareil précis — le parcours biométrique, la régression de mise en page sur petit écran, la vue partagée sur tablette. Ce ne sont pas des unités de travail interchangeables : épinglez-les, et équilibrez ce qui reste.

    La panne qui n'apparaît qu'en parallèle

    Les suites séquentielles masquent l'état partagé. Lancez-en douze à la fois et cela remonte à la surface en une journée.

    Un seul compte, douze sessions. Le grand classique. Le test 7 se connecte, le test 31 connecte le même compte ailleurs, le back-end invalide la première session, et le test 7 échoue sur un écran qui n'a rien à voir avec l'authentification. Chaque relance désigne une victime différente, donc cela ressemble à de l'instabilité. Corrigez avec un pool de comptes loué par partition, pas par suite.

    Jeux de données partagés et modifiables. Un test qui modifie le profil de l'utilisateur de test et un test qui vérifie ce profil cohabitent très bien en séquentiel et deviennent un pile ou face en parallèle. Soit vous initialisez les données par partition, soit chaque test crée ce qu'il vérifie.

    Limites de débit et quotas. Douze partitions qui tapent sur la même API de recette multiplient votre débit de requêtes par douze. Les bacs à sable de paiement et de SMS sont particulièrement prompts à brider — et un 429 se manifeste dans votre application par un écran d'erreur générique, pas par « vous dépassez la limite de débit ».

    État laissé sur l'appareil. Connexions en cache, autorisations accordées, badges de notification, une intégration à moitié terminée. En séquentiel, le test 40 hérite de ce que le test 39 a laissé ; en parallèle, il hérite de ce qu'une autre partition a laissé. Décidez explicitement si chaque partition démarre sur une installation propre ou sur un état chaud connu, et imposez-le à la préparation plutôt que de l'espérer.

    Rien de tout cela n'est un bug de parallélisme. Le parallélisme empêche simplement votre suite de se sérialiser par accident autour de ces problèmes.

    Pourquoi « il suffit d'acheter plus d'exécutions parallèles » est en général la réponse coûteuse

    Sur la plupart des clouds d'appareils, l'unité facturée est la concurrence : vous achetez N créneaux parallèles et la suite se façonne autour de N. Cela a deux conséquences qui se font sentir bien avant la facture.

    La première, c'est la file d'attente. Quand tous les créneaux sont occupés, les tâches attendent — le chiffre qui compte n'est donc pas la durée de vos tests, mais votre délai d'obtention du premier appareil à 16 h, quand tout le monde fusionne ses branches.

    La seconde, c'est que vous commencez à optimiser la mauvaise chose. Dès que la concurrence devient rare, les équipes réduisent la couverture pour tenir dans les créneaux achetés, et « quels tests tournent sur du matériel réel » devient un arbitrage budgétaire plutôt qu'un arbitrage de risque.

    Notre réponse est une unité différente. Ici, les appareils vous sont dédiés et facturés au forfait par appareil et par mois — ni à l'exécution parallèle, ni à la minute, ni au créneau. Un appareil que vous payez est le vôtre, qu'il soit en pleine suite ou inactif à 3 h du matin, ce qui fait du nombre de partitions un paramètre que vous réglez pour le temps d'exécution, et non une ressource que vous rationnez.

    Cela signifie aussi que l'état persiste d'une exécution à l'autre. Une partition n'a pas à réinstaller et à se reconnecter sur une machine fraîchement effacée à chaque fois, ce qui attaque directement ce terme setup_per_device qui mangeait vos gains.

    Le câblage dans la CI

    La matrice est la partie facile. L'exemple est en GitHub Actions, mais la forme est identique en GitLab CI ou Jenkins :

    yaml
    jobs:
      e2e:
        strategy:
          fail-fast: false
          matrix:
            shard: [1, 2, 3, 4, 5, 6]
        steps:
          - uses: actions/checkout@v4
          - run: npm ci
          - run: node shard.mjs --index ${{ matrix.shard }} --total 6 > shard-tests.txt
          - run: npx wdio run wdio.conf.js --spec-file shard-tests.txt
            env:
              GRID_URL: ${{ secrets.ROBOTACTIONS_GRID_URL }}
              GRID_TOKEN: ${{ secrets.ROBOTACTIONS_TOKEN }}
          - uses: actions/upload-artifact@v4
            if: always()
            with:
              name: results-${{ matrix.shard }}
              path: reports/

    Trois détails comptent plus qu'il n'y paraît :

    fail-fast: false — sans quoi la première partition rouge annule les cinq autres et vous obtenez un échec par exécution au lieu du tableau complet. Sur une suite que vous essayez de stabiliser, cela transforme une boucle de débogage d'une heure en une boucle d'une journée.

    if: always() sur l'envoi des artefacts — les résultats des partitions en échec sont précisément ceux dont vous avez besoin.

    Et fusionnez les rapports. Six fichiers JUnit XML distincts dans six artefacts, ce n'est pas un rapport de test : passez-les dans une étape de fusion pour que la pull request obtienne un seul verdict et une seule liste d'échecs.

    Ce qu'il faut faire cette semaine

    1. Enregistrez les durées par test sur une exécution. Il faut ces données avant toute décision.
    2. Trouvez votre test le plus long. C'est votre plancher : découpez-le ou acceptez-le.
    3. Mesurez setup_per_device. Au-delà d'environ 90 secondes, le corriger vaut mieux qu'ajouter des partitions.
    4. Passez du découpage alphabétique à un découpage équilibré par durée. C'est une après-midi, et c'est gratuit.
    5. Ensuite, et seulement ensuite, augmentez le nombre de partitions.

    La plupart des suites ont un confortable facteur 3 à 4 qui dort dans les étapes 2 à 4, avant même d'avoir besoin d'un seul appareil supplémentaire.

    Exécutez votre suite sur des appareils dédiés →

    Prêt à tester sur de vrais appareils ?

    Connectez-vous avec Google ou GitHub et accédez à de vrais appareils iOS et Android depuis votre navigateur — gratuit à l'essai.

    👋 Bonjour! Besoin d'aide? Discutez avec nous!

    Discutez avec nous

    En ligne

    Avant de commencer

    Partagez vos coordonnées pour que nous puissions vous recontacter.