Aumenta la manutenibilità del codice con i test di integrazione di React

Pubblicato: 2022-03-11

I test di integrazione sono un punto debole tra il costo e il valore dei test. La scrittura di test di integrazione per un'app React con l'aiuto della libreria di test di reazione anziché o in aggiunta ai test di unità dei componenti può aumentare la manutenibilità del codice senza compromettere la velocità di sviluppo.

Nel caso in cui desideri ottenere un vantaggio prima di procedere, puoi vedere un esempio di come utilizzare la libreria di test di reazione per i test di integrazione dell'app React qui.

Perché investire nei test di integrazione?

"I test di integrazione trovano un ottimo equilibrio sul compromesso tra fiducia e velocità/spesa. Questo è il motivo per cui è consigliabile spendere la maggior parte (non tutto, intendiamoci) dei tuoi sforzi lì".
– Kent C. Dodds nei test di scrittura. Non troppi. Prevalentemente integrazione.

È pratica comune scrivere unit test per i componenti di React, spesso utilizzando una libreria popolare per testare gli "enzimi" di React; in particolare, il suo metodo "superficiale". Questo approccio ci consente di testare i componenti in isolamento dal resto dell'app. Tuttavia, poiché la scrittura di app React riguarda la composizione di componenti, i test unitari da soli non garantiscono che l'app sia priva di bug.

Ad esempio, la modifica degli prop accettati di un componente e l'aggiornamento dei relativi unit test associati può comportare il superamento di tutti i test mentre l'app potrebbe ancora non funzionare se un altro componente non è stato aggiornato di conseguenza.

I test di integrazione possono aiutare a preservare la tranquillità mentre si apportano modifiche a un'app React, poiché garantiscono che la composizione dei componenti si traduca nell'UX desiderata.

Requisiti per i test di integrazione dell'app React

Ecco alcune delle cose che gli sviluppatori di React vogliono fare quando scrivono i test di integrazione:

  • Testare i casi d'uso dell'applicazione dal punto di vista dell'utente. Gli utenti accedono alle informazioni su una pagina Web e interagiscono con i controlli disponibili.
  • False chiamate API per non dipendere dalla disponibilità e dallo stato dell'API per il superamento o il fallimento dei test.
  • False API del browser (ad esempio, archiviazione locale) poiché semplicemente non esistono nell'ambiente di test.
  • Afferma sullo stato React DOM (browser DOM o un ambiente mobile nativo).

Ora, per alcune cose dovremmo cercare di evitare quando scriviamo i test di integrazione dell'app React:

  • Dettagli di implementazione del test. Le modifiche all'implementazione dovrebbero interrompere un test solo se hanno effettivamente introdotto un bug.
  • Deridere troppo. Vogliamo testare come tutte le parti dell'app funzionano insieme.
  • Rendering poco profondo. Vogliamo testare la composizione di tutti i componenti dell'app fino al componente più piccolo.

Perché scegliere la libreria di test React?

I requisiti di cui sopra rendono la libreria di test di reazione un'ottima scelta, poiché il suo principio guida principale è quello di consentire ai componenti di React di essere testati in un modo che assomigli a come vengono utilizzati da un vero essere umano.

La libreria, insieme alle sue librerie complementari facoltative, ci consente di scrivere test che interagiscono con DOM e ne affermano lo stato.

Esempio di configurazione dell'app

L'app per la quale scriveremo dei test di integrazione di esempio implementa uno scenario semplice:

  • L'utente inserisce un nome utente GitHub.
  • L'app visualizza un elenco di repository pubblici associati al nome utente inserito.

Il modo in cui viene implementata la funzionalità di cui sopra dovrebbe essere irrilevante dal punto di vista del test di integrazione. Tuttavia, per stare vicino alle applicazioni del mondo reale, l'app segue schemi React comuni, da cui l'app:

  • È un'app a pagina singola (SPA).
  • Effettua richieste API.
  • Ha una gestione statale globale.
  • Supporta l'internazionalizzazione.
  • Utilizza una libreria di componenti React.

Il codice sorgente per l'implementazione dell'app è disponibile qui.

Scrivere test di integrazione

Installazione delle dipendenze

Con filato:

 yarn add --dev jest @testing-library/react @testing-library/user-event jest-dom nock

Oppure con npm:

 npm i -D jest @testing-library/react @testing-library/user-event jest-dom nock

Creazione di un file della suite di test di integrazione

Creeremo un file chiamato viewGitHubRepositoriesByUsername.spec.js nella cartella ./test della nostra applicazione. Jest lo raccoglierà automaticamente.

Importazione delle dipendenze nel file di prova

 import React from 'react'; // so that we can use JSX syntax import { render, cleanup, waitForElement } from '@testing-library/react'; // testing helpers import userEvent from '@testing-library/user-event' // testing helpers for imitating user events import 'jest-dom/extend-expect'; // to extend Jest's expect with DOM assertions import nock from 'nock'; // to mock github API import { FAKE_USERNAME_WITH_REPOS, FAKE_USERNAME_WITHOUT_REPOS, FAKE_BAD_USERNAME, REPOS_LIST } from './fixtures/github'; // test data to use in a mock API import './helpers/initTestLocalization'; // to configure i18n for tests import App from '../App'; // the app that we are going to test

Configurazione della suite di test

 describe('view GitHub repositories by username', () => { beforeAll(() => { nock('https://api.github.com') .persist() .get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`) .query(true) .reply(200, REPOS_LIST); }); afterEach(cleanup); describe('when GitHub user has public repositories', () => { it('user can view the list of public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { // arrange // act // assert }); }); });

Appunti:

  • Prima di tutti i test, simula l'API GitHub per restituire un elenco di repository quando viene chiamato con un nome utente specifico.
  • Dopo ogni test, pulisci il test React DOM in modo che ogni test inizi da un punto pulito.
  • describe i blocchi specificare il caso d'uso del test di integrazione e le variazioni di flusso.
  • Le variazioni di flusso che stiamo testando sono:
    • L'utente immette un nome utente valido a cui sono associati repository GitHub pubblici.
    • L'utente immette un nome utente valido che non ha repository GitHub pubblici associati.
    • L'utente inserisce un nome utente che non esiste su GitHub.
  • blocca it uso del callback asincrono poiché il caso d'uso che stanno testando ha un passaggio asincrono al suo interno.

Scrivere il primo test di flusso

Innanzitutto, è necessario eseguire il rendering dell'app.

 const { getByText, getByPlaceholderText, queryByText } = render(<App />);

Il metodo di render importato dal modulo @testing-library/react esegue il rendering dell'app nel test React DOM e restituisce le query DOM associate al contenitore dell'app sottoposta a rendering. Queste query vengono utilizzate per individuare gli elementi DOM con cui interagire e su cui asserire.

Ora, come primo passaggio del flusso in prova, all'utente viene presentato un campo nome utente e digita una stringa nome utente al suo interno.

 userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);

L'helper userEvent dal modulo importato @testing-library/user-event ha un metodo di type che imita il comportamento dell'utente quando digita il testo in un campo di testo. Accetta due parametri: l'elemento DOM che accetta l'input e la stringa che l'utente digita.

Gli utenti di solito trovano gli elementi DOM in base al testo ad essi associato. Nel caso dell'input, si tratta di testo etichetta o testo segnaposto. Il metodo di query getByPlaceholderText restituito in precedenza da render ci consente di trovare l'elemento DOM in base al testo segnaposto.

Si noti che poiché è probabile che il testo stesso cambi, è meglio non fare affidamento sui valori di localizzazione effettivi e configurare invece il modulo di localizzazione in modo che restituisca una chiave dell'elemento di localizzazione come valore.

Ad esempio, quando la localizzazione "en-US" normalmente restituisce Enter GitHub username come valore per la chiave userSelection.usernamePlaceholder , nei test, vogliamo che restituisca userSelection.usernamePlaceholder .

Quando l'utente digita il testo in un campo, dovrebbe vedere il valore del campo di testo aggiornato.

 expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);

Successivamente nel flusso, l'utente fa clic sul pulsante di invio e si aspetta di vedere l'elenco dei repository.

 userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header');

Il metodo userEvent.click imita l'utente che fa clic su un elemento DOM, mentre la query getByText trova un elemento DOM in base al testo che contiene. Il modificatore closest assicura che selezioniamo l'elemento del tipo giusto.

Nota: nei test di integrazione, i passaggi spesso servono sia ad act che assert ruoli. Ad esempio, affermiamo che l'utente può fare clic su un pulsante facendo clic su di esso.

Nel passaggio precedente, abbiamo affermato che l'utente vede la sezione dell'elenco dei repository dell'app. Ora, dobbiamo affermare che poiché il recupero dell'elenco di repository da GitHub potrebbe richiedere del tempo, l'utente vede un'indicazione che il recupero è in corso. Vogliamo anche assicurarci che l'app non comunichi all'utente che non ci sono repository associati al nome utente inserito mentre l'elenco dei repository è ancora in fase di recupero.

 getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull();

Si noti che il prefisso della query getBy viene utilizzato per asserire che l'elemento DOM può essere trovato e il prefisso della query queryBy è utile per l'asserzione opposta. Inoltre, queryBy non restituisce un errore se non viene trovato alcun elemento.

Successivamente, vogliamo assicurarci che, alla fine, l'app finisca di recuperare i repository e li mostri all'utente.

 await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, []));

Il metodo asincrono waitForElement viene utilizzato per attendere un aggiornamento DOM che renderà true l'asserzione fornita come parametro del metodo. In questo caso, affermiamo che l'app visualizza il nome e la descrizione per ogni repository restituito dall'API GitHub simulata.

Infine, l'app non dovrebbe più visualizzare un indicatore che i repository vengono recuperati e non dovrebbe visualizzare un messaggio di errore.

 expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull();

Il nostro test di integrazione React risultante si presenta così:

 it('user can view the list of public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, [])); expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull(); });

Test di flusso alternativo

Quando l'utente immette un nome utente GitHub senza repository pubblici associati, l'app visualizza un messaggio appropriato.

 describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.empty')); expect(queryByText('repositories.error')).toBeNull(); }); });

Quando l'utente immette un nome utente GitHub che non esiste, l'app visualizza un messaggio di errore.

 describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.error')); expect(queryByText('repositories.empty')).toBeNull(); }); });

Perché React Test di integrazione Rock

I test di integrazione offrono davvero un punto debole per le applicazioni React. Questi test aiutano a rilevare i bug e utilizzare l'approccio TDD mentre, allo stesso tempo, non richiedono manutenzione quando l'implementazione cambia.

React-testing-library, mostrato in questo articolo, è un ottimo strumento per scrivere test di integrazione React, in quanto ti consente di interagire con l'app come fa l'utente e di convalidare lo stato e il comportamento dell'app dal punto di vista dell'utente.

Si spera che gli esempi forniti qui ti aiutino a iniziare a scrivere test di integrazione su progetti React nuovi ed esistenti. Il codice di esempio completo che include l'implementazione dell'app è disponibile nel mio GitHub.