Test dei componenti nella pratica

Il test dei componenti è un buon punto di partenza per dimostrare il codice dei test pratici. I test dei componenti sono più sostanziali rispetto ai semplici test delle unità, sono meno complessi dei test end-to-end e dimostrano l'interazione con il DOM. In filosofia, l'uso di React ha permesso agli sviluppatori web di pensare più facilmente ai siti web o alle app web come costituiti da componenti.

Pertanto, testare i singoli componenti, indipendentemente dalla loro complessità, è un buon modo per iniziare a pensare di testare un'applicazione nuova o esistente.

In questa pagina vengono illustrati i test di un piccolo componente con dipendenze esterne complesse. È facile testare un componente che non interagisce con altri codici, ad esempio facendo clic su un pulsante e confermando che un numero aumenta. In realtà, questo è davvero poco codice e testare un codice che non ha interazioni può avere un valore limitato.

Questo non è inteso come un tutorial completo e una sezione successiva, Test automatici in pratica, ti guiderà attraverso il test di un sito reale con un codice campione che puoi utilizzare come tutorial. Tuttavia, questa pagina tratterà comunque diversi esempi di test pratici dei componenti.

Il componente in fase di test

Utilizzeremo Vitest e il suo ambiente JSDOM per testare un componente React. In questo modo, puoi eseguire rapidamente i test utilizzando Node sulla riga di comando durante l'emulazione di un browser.

Un elenco di nomi con un pulsante Scegli accanto a ogni nome.
Un piccolo componente di React che mostra un elenco di utenti della rete.

Questo componente di React denominato UserList recupera un elenco di utenti dalla rete e ti consente di selezionarne uno. L'elenco di utenti viene ottenuto utilizzando fetch all'interno di un useEffect e il gestore di selezione viene trasmesso da Context. Questo è il codice:

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

Questo esempio non dimostra le best practice di React (ad esempio utilizza fetch all'interno di useEffect), ma è probabile che il tuo codebase contenga molti casi simili. Soprattutto, questi casi possono sembrare testati da testare a colpo d'occhio. In una sezione futura di questo corso parleremo in dettaglio della scrittura di codice testabile.

Ecco gli elementi che stiamo testando in questo esempio:

  • Verifica che vengano creati alcuni DOM corretti in risposta ai dati della rete.
  • Verifica che facendo clic su un utente venga attivato un callback.

Ogni componente è diverso. Perché è interessante testare questo aspetto?

  • Utilizza l'elemento fetch globale per richiedere alla rete dati reali, che durante il test potrebbero risultare instabili o lenti.
  • Importa un'altra classe, UserRow, che potremmo non voler testare implicitamente.
  • Utilizza un elemento Context che non fa parte specificamente del codice in fase di test e è normalmente fornito da un componente principale.

Scrivi un test rapido per iniziare

Possiamo rapidamente testare qualcosa di molto basilare su questo componente. Precisiamo che questo esempio non è molto utile. Tuttavia è utile configurare il boilerplate in un file peer denominato UserList.test.tsx (ricorda che i runner di test come Vitest eseguiranno, per impostazione predefinita, file che terminano con .test.js o simili, tra cui .tsx):

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

Questo test afferma che, quando viene eseguito il rendering, il componente contiene il testo "Utenti". Funziona anche se il componente ha l'effetto collaterale di inviare fetch alla rete. fetch è ancora in corso al termine del test, senza endpoint impostato. Non possiamo confermare che al termine del test vengano mostrate informazioni relative agli utenti, almeno non senza attendere un timeout.

Simulazione fetch()

Per falsificazione si intende l'atto di sostituire una funzione o una classe reali con qualcosa sotto il tuo controllo per un test. Si tratta di una prassi comune in quasi tutti i tipi di test, ad eccezione di quelli più semplici. Questo argomento sarà trattato in maniera più approfondita in Asserzioni e altre primitive.

Puoi simulare fetch() per il tuo test in modo che venga completato rapidamente e restituisca i dati previsti e non quelli "reali" o sconosciuti. fetch è un elemento globale, il che significa che non è necessario import o require nel nostro codice.

In vitest, puoi simulare un globale chiamando vi.stubGlobal con un oggetto speciale restituito da vi.fn() per creare una simulazione che possiamo modificare in seguito. Questi metodi verranno esaminati in maggiore dettaglio in una sezione successiva di questo corso, ma puoi trovarli in pratica nel codice seguente:

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

Questo codice aggiunge una simulazione, descrive una versione "falsa" del recupero della rete Response, quindi attende che venga visualizzata. Se il testo non viene visualizzato, puoi verificarlo modificando il nome della query in queryByText: il test avrà esito negativo.

In questo esempio sono stati utilizzati gli aiutanti di simulazione integrati di Vitest, ma altri framework di test hanno approcci simili alle deridere. Vitest è univoco in quanto devi chiamare vi.unstubAllGlobals() dopo tutti i test oppure impostare un'opzione globale equivalente. Se non "annullare" il nostro lavoro, la simulazione di fetch può influire su altri test e a ogni richiesta verrà inviata una risposta con la nostra strana pila di JSON.

Importazioni fittizie

Potresti aver notato che il nostro componente UserList importa un componente chiamato UserRow. Anche se non abbiamo incluso il codice, puoi vedere che viene visualizzato il nome dell'utente: il test precedente verifica la presenza di "Sam" e che non viene eseguito direttamente all'interno di UserList, quindi deve provenire da UserRow.

Un diagramma di flusso che mostra come si muovono i nomi degli utenti all&#39;interno del nostro componente.
UserListTest non ha visibilità di UserRow.

Tuttavia, UserRow potrebbe essere di per sé un componente complesso, in quanto potrebbe recuperare ulteriori dati utente o avere effetti collaterali non pertinenti per il nostro test. La rimozione di questa variabilità renderà i test più utili, soprattutto perché i componenti che vuoi testare sono più complessi e si intrecciano con le loro dipendenze.

Fortunatamente, puoi utilizzare Vitest per simulare alcune importazioni, anche se il test non le utilizza direttamente, in modo che qualsiasi codice che le utilizzi sia fornito con una versione semplice o nota:

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

Come un esempio di simulazione del test globale fetch, anche questo è uno strumento potente, ma può diventare insostenibile se il codice ha molte dipendenze. Anche in questo caso, la soluzione migliore è scrivere codice testabile.

Fai clic e fornisci il contesto

React e altre librerie come Lit hanno un concetto chiamato Context. Il codice di esempio include un UserContext che chiama un metodo se viene scelto un utente. Questo viene spesso visto come un'alternativa alla "perforazione degli oggetti di scena", in cui il callback viene trasmesso direttamente a UserList.

Il set di test che abbiamo scritto non ha fornito UserContext. Se aggiungi un'azione di clic al test React senza questa funzione, nella peggiore delle ipotesi si verificherà un arresto anomalo del test o, nella migliore delle ipotesi, se un'istanza predefinita è stata fornita altrove, causerà un comportamento fuori dal nostro controllo (simile a un UserRow sconosciuto di cui sopra):

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

Quando esegui il rendering del componente, puoi fornire invece il tuo Context. Questo esempio utilizza un'istanza di vi.fn(), una funzione fittizia di Vitest, che può essere utilizzata dopo il fact checking per verificare che sia stata effettuata una chiamata e con quali argomenti è stata fatta. Nel nostro caso, l'oggetto interagisce con l'oggetto fetch simulato nell'esempio precedente e il test può confermare che l'ID trasmesso era "sam":

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

Si tratta di un pattern semplice ma potente che ti consente di rimuovere dipendenze irrilevanti dal componente principale che stai tentando di testare.

In sintesi

Questo è un esempio rapido e semplificato che mostra come creare un test dei componenti per testare e salvaguardare un componente React difficile da testare, concentrandosi sul fatto che il componente interagisca correttamente con le sue dipendenze (fetch globale, un sottocomponente importato e un Context).

Verifica le tue conoscenze

Quali approcci sono stati utilizzati per testare il componente React?

Simulazione di dipendenze complesse con semplici per il test
Iniezione di dipendenze mediante il contesto
Tutto il mondo
Verifica dell'incremento di un numero