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.
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
.
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?