Test dei componenti nella pratica

I test dei componenti sono un buon punto di partenza per dimostrare il codice di test pratico. I test dei componenti sono più sostanziali dei semplici test di unità, meno complessi dei test end-to-end e dimostrano l'interazione con il DOM. A livello più filosofico, l'utilizzo di React ha consentito agli sviluppatori web di considerare più facilmente i siti web o le app web come composti da componenti.

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

Questa pagina illustra la procedura per testare un piccolo componente con dipendenze esterne complesse. È facile testare un componente che non interagisce con nessun altro codice, ad esempio facendo clic su un pulsante e verificando che un numero aumenti. In realtà, pochissimo codice è simile e il test del codice che non ha interazioni può essere di scarso valore.

Il componente in test

Utilizziamo Vitest e il suo ambiente JSDOM per testare un componente React. In questo modo possiamo eseguire rapidamente i test utilizzando Node sulla riga di comando e emulazione di un browser.

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

Questo componente 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 della selezione viene passato da Context. Ecco 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 mostra le best practice per le reazioni (ad esempio, utilizza fetch all'interno di useEffect), ma è probabile che il tuo codebase contenga molti casi simili. Più precisamente, questi casi possono sembrare difficili da testare a prima vista. In una futura sezione di questo corso verrà illustrata nel dettaglio la scrittura di codice testabile.

Ecco gli elementi che stiamo testando in questo esempio:

  • Verifica che venga creato un DOM corretto in risposta ai dati della rete.
  • Verifica che il clic di un utente attivi un callback.

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

  • Utilizza il valore fetch globale per richiedere dati reali dalla rete, che potrebbe essere instabile o lenta durante il test.
  • Importa un'altra classe, UserRow, che potremmo non voler testare in modo implicito.
  • Utilizza un Context che non fa parte specificamente del codice in test e viene normalmente fornito da un componente principale.

Scrivi un rapido test per iniziare

Possiamo testare rapidamente un aspetto molto basilare di questo componente. Precisiamo che questo esempio non è molto utile. Tuttavia, è utile configurare il boilerplate in un file peer chiamato UserList.test.tsx (ricorda che i runner di test come Vitest, per impostazione predefinita, eseguono file che terminano con .test.js o simili, incluso .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 un fetch alla rete. Al termine del test, fetch è ancora in corso, senza un endpoint impostato. Non possiamo confermare che le informazioni dell'utente vengano mostrate al termine del test, almeno non senza attendere un timeout.

Simulazione fetch()

Il mocking consiste nel sostituire una funzione o una classe reale con qualcosa sotto il tuo controllo per un test. Questa è una pratica comune in quasi tutti i tipi di test, tranne che per i test di unità più semplici. Scopri di più in Verifiche e altre primitive.

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

In vitest, puoi creare un modello globale chiamando vi.stubGlobal con un oggetto speciale restituito da vi.fn(), che crea un modello che possiamo modificare in un secondo momento. Questi metodi vengono esaminati più dettagliatamente in una sezione successiva di questo corso, ma puoi vederli in pratica nel seguente codice:

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 cambiando la query in queryByText con un nuovo nome. Il test non andrà a buon fine.

Questo esempio ha utilizzato gli helper di simulazione integrati di Vitest, ma altri framework di test hanno approcci simili alla simulazione. Vitest è unico in quanto devi chiamare vi.unstubAllGlobals() dopo tutti i test o impostare un'opzione globale equivalente. Senza "annullare" il nostro lavoro, il mock fetch può influire su altri test e a ogni richiesta verrà data risposta con il nostro strano mucchio di JSON.

Importazioni simulate

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

Un diagramma di flusso che mostra come i nomi degli utenti vengono gestiti nel nostro componente.
UserListTest non ha visibilità di UserRow.

Tuttavia, UserRow potrebbe essere un componente complesso, che potrebbe recuperare ulteriori dati utente o avere effetti collaterali non pertinenti per il nostro test. La rimozione di questa variabilità rende i test più utili, soprattutto perché i componenti che vuoi testare diventano più complessi e più legati alle loro dipendenze.

Fortunatamente, puoi utilizzare Vitest per simulare determinate importazioni, anche se il tuo test non le utilizza direttamente, in modo che ogni codice che le utilizza venga fornito con una versione semplice o nota:

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

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

Come nel caso di una simulazione del fetch globale, questo è uno strumento potente, ma può diventare insostenibile se il codice ha molte dipendenze. Anche in questo caso, la soluzione migliore è scrivere codice verificabile.

Fai clic e fornisci il contesto

React e altre librerie come Lit hanno un concetto chiamato Context. Il codice di esempio include UserContext, che invoca il metodo se viene scelto un utente. Spesso viene considerata un'alternativa alla "prop drilling", in cui il callback viene passato direttamente a UserList.

Il nostro test harness non ha fornito UserContext. Se aggiungi un'azione di clic al test di reazione senza questa, questa operazione potrebbe causare l'arresto anomalo del test. Al meglio, se un'istanza predefinita è stata fornita altrove, potrebbe causare alcuni comportamenti al di fuori del nostro controllo (come un UserRow sconosciuto sopra).

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

Al contrario, durante il rendering del componente, puoi fornire il tuo Context. Questo esempio utilizza un'istanza di vi.fn(), una funzione simulata Vitest, che può essere utilizzata per verificare che sia stata effettuata una chiamata e quali argomenti sono stati utilizzati.

Nel nostro caso, interagisce con fetch simulato nell'esempio precedente e il test può confermare che l'ID passato è 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 efficace che può consentirti di rimuovere le dipendenze non pertinenti dal componente principale che stai tentando di testare.

In sintesi

Questo esempio ha dimostrato come creare un test dei componenti per testare e salvaguardare un componente React difficile da testare. Questo test si è concentrato sull'assicurarsi che il componente interagisca correttamente con le sue dipendenze: il parametro globale fetch, un sottocomponente importato e un Context.

Verificare di aver compreso

Quali approcci sono stati utilizzati per testare il componente React?

Simulazione di dipendenze complesse con quelle semplici per il test
Iniezione di dipendenze tramite Context
Simulazione di variabili globali
Verificare l'incremento di un numero