1 octobre 2019
Tester son app React Native - End-to-End avec Detox
5 minutes de lecture
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 :
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 ! 🔥
👋