Picasso : comment tester une bibliothèque de composants

Publié: 2022-03-11

Une nouvelle version du système de conception de Toptal a été publiée récemment, ce qui nous obligeait à apporter des modifications à presque tous les composants de Picasso, notre bibliothèque de composants interne. Notre équipe a été confrontée à un défi : comment s'assurer que les régressions ne se produisent pas ?

La réponse courte est, sans surprise, des tests. Beaucoup d'épreuves.

Nous ne passerons pas en revue les aspects théoriques des tests, ni ne discuterons des différents types de tests, de leur utilité, ni n'expliquerons pourquoi vous devriez tester votre code en premier lieu. Notre blog et d'autres ont déjà couvert ces sujets. Au lieu de cela, nous nous concentrerons uniquement sur les aspects pratiques des tests.

Lisez la suite pour savoir comment les développeurs de Toptal écrivent des tests. Notre référentiel est public, nous utilisons donc des exemples concrets. Il n'y a pas d'abstractions ou de simplifications.

Pyramide des tests

Nous n'avons pas défini de pyramide de test en soi, mais si nous l'avions fait, cela ressemblerait à ceci :

Test de l'illustration de la pyramide

La pyramide des tests de Toptal illustre les tests sur lesquels nous insistons.

Tests unitaires

Les tests unitaires sont simples à écrire et faciles à exécuter. Si vous avez très peu de temps pour écrire des tests, ils devraient être votre premier choix.

Cependant, ils ne sont pas parfaits. Quelle que soit la bibliothèque de test que vous choisissez (Jest and React Testing Library [RTL] dans notre cas), elle n'aura pas de véritable DOM et ne vous permettra pas de vérifier les fonctionnalités dans différents navigateurs, mais vous permettra de supprimer éliminez la complexité et testez les blocs de construction simples de votre bibliothèque.

Les tests unitaires n'ajoutent pas seulement de la valeur en testant le comportement du code, mais également en vérifiant la testabilité globale du code. Si vous ne pouvez pas écrire facilement des tests unitaires, il y a de fortes chances que vous ayez un mauvais code.

Tests de régression visuelle

Même si vous disposez d'une couverture de test unitaire à 100 %, cela ne signifie pas que les composants s'affichent bien sur tous les appareils et navigateurs.

Les régressions visuelles sont particulièrement difficiles à repérer avec les tests manuels. Par exemple, si l'étiquette d'un bouton est déplacée de 1 px, un ingénieur QA le remarquera-t-il même ? Heureusement, il existe de nombreuses solutions à ce problème de visibilité limitée. Vous pouvez opter pour des solutions tout-en-un d'entreprise telles que LambdaTest ou Mabl. Vous pouvez incorporer des plugins, comme Percy, dans vos tests existants, ainsi que des solutions de bricolage comme Loki ou Storybook (ce que nous utilisions avant Picasso). Ils ont tous des inconvénients : certains sont trop chers tandis que d'autres ont une courbe d'apprentissage abrupte ou nécessitent trop d'entretien.

Happo à la rescousse ! C'est un concurrent direct de Percy, mais il est beaucoup moins cher, prend en charge plus de navigateurs et est plus facile à utiliser. Un autre gros argument de vente ? Il prend en charge l'intégration de Cypress, ce qui était important car nous voulions nous éloigner de l'utilisation de Storybook pour les tests visuels. Nous nous sommes retrouvés dans des situations où nous devions créer des histoires uniquement pour pouvoir assurer une couverture visuelle des tests, et non parce que nous devions documenter ce cas d'utilisation. Cela a pollué nos documents et les a rendus plus difficiles à comprendre. Nous voulions isoler les tests visuels de la documentation visuelle.

Essais d'intégration

Même si deux composants ont des tests unitaires et visuels, cela ne garantit pas qu'ils fonctionneront ensemble. Par exemple, nous avons trouvé un bogue où une info-bulle ne s'ouvre pas lorsqu'elle est utilisée dans un élément déroulant, mais fonctionne bien lorsqu'elle est utilisée seule.

Pour nous assurer que les composants s'intègrent bien, nous avons utilisé la fonction de test de composants expérimentaux de Cypress. Au début, nous étions mécontents des mauvaises performances, mais nous avons pu les améliorer avec une configuration webpack personnalisée. Le résultat? Nous avons pu utiliser l'excellente API de Cypress pour écrire des tests performants qui garantissent que nos composants fonctionnent bien ensemble.

Application de la pyramide des tests

À quoi tout cela ressemble-t-il dans la vraie vie ? Testons le composant Accordion !

Votre premier réflexe pourrait être d'ouvrir votre éditeur et de commencer à écrire du code. Mon conseil? Passez un peu de temps à comprendre toutes les fonctionnalités du composant et notez les cas de test que vous souhaitez couvrir.

GIF de démonstration de la bibliothèque de composants Picasso

Quoi tester ?

Voici une ventilation des cas que nos tests devraient couvrir :

  • États - Les accordéons peuvent être développés et réduits, son état par défaut peut être configuré et cette fonctionnalité peut être désactivée
  • Styles - Les accordéons peuvent avoir des variations de bordure
  • Contenu – Ils peuvent s'intégrer à d'autres unités de la bibliothèque
  • Personnalisation - Le composant peut avoir ses styles remplacés et peut avoir des icônes de développement personnalisées
  • Rappels - Chaque fois que l'état change, un rappel peut être invoqué

GIF de démonstration de la bibliothèque de composants Picasso - composant d'accordéon

Comment tester ?

Maintenant que nous savons ce que nous devons tester, voyons comment procéder. Nous avons trois options dans notre pyramide de test. Nous voulons obtenir une couverture maximale avec un chevauchement minimal entre les sections de la pyramide. Quelle est la meilleure façon de tester chaque cas de test ?

  • États - Les tests unitaires peuvent nous aider à évaluer si les états changent en conséquence, mais nous avons également besoin de tests visuels pour nous assurer que le composant est rendu correctement dans chaque état.
  • Styles – Des tests visuels devraient suffire à détecter les régressions des différentes variantes
  • Contenu - Une combinaison de tests visuels et d'intégration est le meilleur choix, car Accordions peut être utilisé en combinaison avec de nombreux autres composants
  • Personnalisation - Nous pouvons utiliser un test unitaire pour vérifier si un nom de classe est appliqué correctement, mais nous avons besoin d'un test visuel pour nous assurer que le composant et les styles personnalisés fonctionnent en tandem.
  • Rappels - Les tests unitaires sont idéaux pour s'assurer que les bons rappels sont invoqués

La pyramide des tests d'accordéon

Tests unitaires

La suite complète des tests unitaires peut être trouvée ici. Nous avons couvert tous les changements d'état, la personnalisation et les rappels :

 it('toggles', async () => { const handleChange = jest.fn() const { getByText, getByTestId } = renderAccordion({ onChange: handleChange, expandIcon: <span data-test /> }) fireEvent.click(getByTestId('accordion-summary')) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) fireEvent.click(getByTestId('trigger')) await waitFor(() => expect(getByText(DETAILS_TEXT)).not.toBeVisible()) fireEvent.click(getByText(SUMMARY_TEXT)) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) expect(handleChange).toHaveBeenCalledTimes(3) })

Tests de régression visuelle

Les tests visuels sont situés dans ce bloc de description Cypress. Les captures d'écran peuvent être trouvées dans le tableau de bord de Happo.

Vous pouvez voir que tous les différents états, variantes et personnalisations des composants ont été enregistrés. Chaque fois qu'un PR est ouvert, CI compare les captures d'écran stockées par Happo à celles prises dans votre agence :

 it('renders', () => { mount( <TestingPicasso> <TestAccordion /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders disabled', () => { mount( <TestingPicasso> <TestAccordion disabled /> <TestAccordion expandIcon={<Check16 />} /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders border variants', () => { mount( <TestingPicasso> <TestAccordion borders='none' /> <TestAccordion borders='middle' /> <TestAccordion borders='all' /> </TestingPicasso> ) cy.get('body').happoScreenshot() })

Essais d'intégration

Nous avons écrit un test de "mauvais chemin" dans ce bloc de description Cypress qui affirme que l'accordéon fonctionne toujours correctement et que les utilisateurs peuvent interagir avec le composant personnalisé. Nous avons également ajouté des assertions visuelles pour plus de confiance :

 describe('Accordion with custom summary', () => { it('closes and opens', () => { mount(<AccordionCustomSummary />) toggleAccordion() getAccordionContent().should('not.be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() toggleAccordion() getAccordionContent().should('be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() }) // … })

Intégration continue

Picasso s'appuie presque entièrement sur GitHub Actions pour l'assurance qualité. De plus, nous avons ajouté des crochets Git pour les vérifications de la qualité du code des fichiers mis en scène. Nous avons récemment migré de Jenkins vers GHA, notre configuration est donc toujours au stade MVP.

Le flux de travail est exécuté à chaque changement dans la succursale distante dans un ordre séquentiel, les tests d'intégration et visuels étant la dernière étape car ils sont les plus coûteux à exécuter (à la fois en termes de performances et de coût monétaire). À moins que tous les tests ne se terminent avec succès, la demande d'extraction ne peut pas être fusionnée.

Voici les étapes que GitHub Actions traverse à chaque fois :

  1. Installation de dépendance
  2. Contrôle de version – Valide que le format des commits et le titre du PR correspondent aux commits conventionnels
  3. Lint – ESlint garantit un code de bonne qualité
  4. Compilation TypeScript - Vérifiez qu'il n'y a pas d'erreurs de type
  5. Compilation de packages - Si les packages ne peuvent pas être construits, ils ne seront pas publiés avec succès; nos tests Cypress attendent également du code compilé
  6. Tests unitaires
  7. Intégration et tests visuels

Le flux de travail complet peut être trouvé ici. Actuellement, il faut moins de 12 minutes pour terminer toutes les étapes.

Testabilité

Comme la plupart des bibliothèques de composants, Picasso possède un composant racine qui doit envelopper tous les autres composants et peut être utilisé pour définir des règles globales. Cela rend plus difficile l'écriture de tests pour deux raisons : des incohérences dans les résultats des tests, selon les accessoires utilisés dans le wrapper ; et passe-partout supplémentaire :

 import { render } from '@testing-library/react' describe('Form', () => { it('renders', () => { const { container } = render( <Picasso loadFavicon={false} environment='test'> <Form /> </Picasso> ) expect(container).toMatchSnapshot() }) })

Nous avons résolu le premier problème en créant un TestingPicasso qui préconditionne les règles globales de test. Mais c'est ennuyeux de devoir le déclarer pour chaque cas de test. C'est pourquoi nous avons créé une fonction de rendu personnalisée qui encapsule le composant passé dans un TestingPicasso et renvoie tout ce qui est disponible à partir de la fonction de rendu de RTL.

Nos tests sont maintenant plus faciles à lire et simples à écrire :

 import { render } from '@toptal/picasso/test-utils' describe('Form', () => { it('renders', () => { const { container } = render(<Form />) expect(container).toMatchSnapshot() }) })

Conclusion

La configuration décrite ici est loin d'être parfaite, mais c'est un bon point de départ pour ceux d'entre vous qui sont assez aventureux pour créer une bibliothèque de composants. J'ai beaucoup lu sur le test des pyramides, mais ce n'est pas toujours facile de les appliquer dans la pratique. Par conséquent, je vous invite à explorer notre base de code et à apprendre de nos erreurs et de nos succès.

Les bibliothèques de composants sont uniques car elles desservent deux types d'audience : les utilisateurs finaux qui interagissent avec l'interface utilisateur et les développeurs qui utilisent votre code pour créer leurs propres applications. Investir du temps dans un cadre de test robuste profitera à tout le monde. Investir du temps dans des améliorations de testabilité vous sera bénéfique en tant que mainteneur et aux ingénieurs qui utilisent (et testent) votre bibliothèque.

Nous n'avons pas discuté de choses comme la couverture du code, les tests de bout en bout et les politiques de version et de publication. Le petit conseil sur ces sujets est le suivant : publiez souvent, pratiquez une version sémantique appropriée, ayez de la transparence dans vos processus et définissez des attentes pour les ingénieurs qui s'appuient sur votre bibliothèque. Nous reviendrons peut-être sur ces sujets plus en détail dans des articles ultérieurs.