Come scrivere test efficaci per il frontend con Jest e React Testing Library

L’importanza del testing nello sviluppo di applicazioni basate su React

Introduzione al Testing

Il testing è una fase cruciale nel processo di sviluppo delle applicazioni basate su React, così come in qualsiasi altro ambito dello sviluppo software. React utilizza una struttura basata sui componenti, il che significa che una singola modifica a un componente potrebbe potenzialmente influenzare gli altri ed è proprio questa interdipendenza a rendere il testing particolarmente importante. Nel caso in cui un componente non superi un test, quest’ultimo notificherà immediatamente lo sviluppatore (o il tester), permettendo a chi di dovere di correggere rapidamente il problema e ridurre al minimo eventuali interruzioni del lavoro.

È infatti comune tra gli sviluppatori meno esperti sottovalutare l'importanza del testing, rischiando di perdere molto più tempo, a lungo termine, durante la fase di debugging e nel fix di problemi sovvenuti una volta in produzione; infatti senza test adeguati, gli errori possono passare inosservati fino a stadi avanzati dello sviluppo, causando interruzioni maggiori e complicazioni nella risoluzione dei bug, oltre alla possibilità di dimostrarsi poco abili agli occhi di eventuali clienti. Al contrario, un'accurata fase di testing consente di identificare e correggere rapidamente le problematiche, migliorando la qualità complessiva del codice e accelerando il processo di sviluppo nel lungo periodo.

Testing.png

TDD vs BDD: Quale approccio scegliere?

Quando si decide di sviluppare un software, si possono seguire due strade: TDD (Test Driven Development) oppure BDD (Behavior Driven Development). 

tdd-bdd.png

Entrambi si focalizzano sul testing (e collaborazione), ma col primo, gli sviluppatori testano fin da subito, e sono i risultati dei test a guidare lo sviluppo che ne consegue: il flusso di lavoro segue quindi un ciclo Red-Green-Refactor, dove si scrive un test che fallisce (Red), si implementa il codice minimo necessario per farlo passare (Green), e infine si ottimizza il codice senza modificarne il comportamento (Refactor).

Nel BDD invece, ci si concentra maggiormente sul comportamento dell’applicazione, dal punto di vista dell’utente, permettendo una maggiore collaborazione tra sviluppatori, tester e stakeholder; difatti, si utilizza un linguaggio più naturale: “Dato un utente che visita una pagina..”, “Quando clicco sul pulsante ‘Aggiungi’..” ecc.

Se masticate un po’ di inglese e volete approfondire l’argomento, di seguito lascio i link per degli articoli molti interessanti in merito:

React Testing Library e Jest: perché usarli insieme

Nel contesto di React il metodo generalmente preferito è il BDD (anche se il TDD può essere utilizzato), e React Testing Library e Jest sono due strumenti fondamentali nel processo di testing di un'applicazione React, e usarli insieme offre numerosi vantaggi. La prima libreria si concentra sulla simulazione del comportamento dell'utente, fornendo metodi per testare il rendering e l'interazione con i componenti come farebbe un utente finale. Jest, invece, è un framework di testing completo che gestisce l'esecuzione dei test, la creazione di snapshot e il mocking delle funzioni. E’ uso comune utilizzarli insieme poiché React Testing Library permette di testare l’interazione con il DOM, mentre Jest si occupa della logica e dell'esecuzione dei test, facilitando anche il monitoraggio dei risultati. Questa combinazione consente di scrivere test più intuitivi, leggeri e manutenibili, migliorando l'affidabilità del codice e semplificando la gestione del ciclo di vita dei test.

Setup di React Testing Library e Jest

Per configurarli, il primo passo è assicurarsi che entrambe le librerie siano installate. Creando il progetto con Create React App, Jest è già incluso di default, mentre React Testing Library è spesso preinstallata. Se non lo fosse, è possibile installarla manualmente eseguendo il comando:

npm install --save-dev @testing-library/react @testing-library/jest-dom

Dopo l'installazione, è buona pratica configurare Jest per riconoscere React Testing Library. Per farlo, va creato (o modificato) il file setupTests.js nella cartella src, aggiungendo questa riga:

import '@testing-library/jest-dom';

Jest cercherà i file di test seguendo una delle seguenti convenzioni di denominazione più comuni:

  • File con estensione .js all'interno di cartelle tests.
  • File con suffisso .test.js.
  • File con suffisso .spec.js.

I file .test.js / .spec.js (o le cartelle tests) possono trovarsi a qualsiasi profondità all'interno della cartella src.

Per progetti più grandi, si consiglia di organizzare tutti i file .test.js / .spec.js all'interno di cartelle tests sotto la cartella src.

Guida allo sviluppo e al testing di un componente Stepper in React

Per dimostrare le potenzialità di questi strumenti, li utilizzeremo cercando di sviluppare un classico componente che si può trovare in un real-world project: lo Stepper.

Lo Stepper è un componente che consente la navigazione tra una serie di passi predefiniti, tra cui possono alternarsi moduli o simili. L'utente può avanzare o tornare indietro utilizzando due pulsanti, con la logica necessaria per disabilitarli ai limiti della sequenza.

Obiettivi dei test:

  • Verificare che il primo passo venga visualizzato inizialmente.
  • Controllare il funzionamento dei pulsanti "Avanti" e "Indietro".
  • Assicurarsi che i pulsanti si disabilitino correttamente ai limiti della navigazione.

Componente base:

    {`import { useState } from "react";

    type StepperProps = {
        steps: string[];
    };

    const Stepper = ({ steps }: StepperProps) => {
        const [currentStep, setCurrentStep] = useState(0);

        return (
        <div>
            <h2 data-testid="step-title">{steps[currentStep]}</h2>

            <button 
            onClick={() => setCurrentStep((prev) => prev - 1)} 
            disabled={currentStep === 0}
            >
            Indietro
            </button>

            <button 
            onClick={() => setCurrentStep((prev) => prev + 1)} 
            disabled={currentStep === steps.length - 1} 
            >
            Avanti
            </button>
        </div>
        );
    };

    export default Stepper;`}

Prima di tutto gettiamo le basi del componente, sfruttando lo stato di React per lo step e lo stato attivato/disabilitato del bottone. 

Scrittura dei test con Jest e React Testing Library

I test controlleranno il rendering iniziale e il comportamento dei pulsanti durante la navigazione.

    {`import { render, screen, fireEvent } from "@testing-library/react";
    import Stepper from "./Stepper";

    describe("Stepper Component", () => {
        const steps = ["Passo 1", "Passo 2", "Passo 3"];
        test("renders the first step initially", () => {
        render(<Stepper steps={steps} />);
        expect(screen.getByTestId("step-title")).toHaveTextContent("Passo 1");
        });

        test("advances to the next step when 'Avanti' is clicked", () => {
        render(<Stepper steps={steps} />);
        const nextButton = screen.getByText("Avanti");
        fireEvent.click(nextButton);
        expect(screen.getByTestId("step-title")).toHaveTextContent("Passo 2");
        });

        test("goes back to the previous step when 'Indietro' is clicked", () => {
        render(<Stepper steps={steps} />);
        const nextButton = screen.getByText("Avanti");
        fireEvent.click(nextButton); // Vai al passo 2
        fireEvent.click(screen.getByText("Indietro")); // Torna al passo 1
        expect(screen.getByTestId("step-title")).toHaveTextContent("Passo 1");
        });

        test("disables 'Indietro' on the first step and 'Avanti' on the last step", () => {
        render(<Stepper steps={steps} />);
        expect(screen.getByText("Indietro")).toBeDisabled(); // Primo step
        fireEvent.click(screen.getByText("Avanti"));
        fireEvent.click(screen.getByText("Avanti")); // Vai all'ultimo step
        expect(screen.getByText("Avanti")).toBeDisabled(); // Ultimo step
        });

    });`}

Dopo aver correttamente configurato i file, possiamo eseguire il comando:

npm test

Jest troverà automaticamente i file .test.tsx e li eseguirà, fornendo un report dettagliato nel terminale.

Output:

FAIL  Stepper.test.tsx

  Stepper Component

    ✓ renders the first step initially (23 ms)

    ✓ advances to the next step when 'Avanti' is clicked (18 ms)

    ✓ goes back to the previous step when 'Indietro' is clicked (15 ms)

    ✕ disables 'Indietro' on the first step and 'Avanti' on the last step (12 ms)

Stepper Component › disables 'Indietro' on the first step and 'Avanti' on the last step

    expect(received).toBeDisabled()
    Received element is not disabled:
    <button>Avanti</button>

    at Object.<anonymous> (Stepper.test.tsx:27:35)

Analisi dell'Errore

Il test sta fallendo perché il pulsante "Avanti" non viene disabilitato correttamente all'ultimo step. E’ logico quindi che il problema è nella condizione:

disabled={currentStep > steps.length - 1}

mentre dovrebbe essere:

>disabled={currentStep === steps.length - 1}

Ora, se rieseguiamo il comando npm test, i test dovrebbero passare con successo:

 PASS  Stepper.test.tsx

Stepper Component

✓ renders the first step initially (23 ms)

✓ advances to the next step when 'Avanti' is clicked (18 ms)

✓ goes back to the previous step when 'Indietro' is clicked (15 ms)

✓ disables 'Indietro' on the first step and 'Avanti' on the last step (12 ms)

In questo modo, siamo riusciti a individuare e correggere un bug in pochi minuti.

Questo è solo un piccolo esempio per dimostrare le potenzialità dei test: proporzionalmente alla complessità e alla struttura di un progetto, queste tecnologie migliorano l’efficienza del testing automatico nel rilevare errori logici, permettendo di risparmiare tempo e risorse.

Conclusione

Incorporare una strategia di testing efficace fin dalle prime fasi dello sviluppo porta numerosi vantaggi: migliora la manutenibilità del codice, accelera il debugging e contribuisce alla stabilità generale del progetto. Anche se abituarsi al boilerplate di queste tecnologie e il setup e scrittura dei test può sembrare un’attività dispendiosa e complessa all’inizio, il tempo investito si traduce in una riduzione significativa dei problemi a lungo periodo. 

Se non hai ancora integrato il testing nel tuo workflow, prova a iniziare con piccoli test su componenti più critici e sperimenta con gli strumenti elencati. 

Il testing non è solo una best practice, ma un vero e proprio alleato per lo sviluppo di software affidabili e scalabili!

Autoreadmin
Potrebbero interessarti...