Strumenti del mestiere

I test automatici sono fondamentalmente solo codice che genera o genera un errore se si verifica un problema. La maggior parte delle librerie o dei framework di test fornisce una varietà di primitivi che semplificano la scrittura dei test.

Come accennato nella sezione precedente, queste primitive includono quasi sempre un modo per definire test indipendenti (chiamati casi di test) e per fornire affermazioni. Le asserzioni sono un modo per combinare il controllo di un risultato con la generazione di un errore se qualcosa non va e possono essere considerate la primitiva di base di tutte le primitive di test.

In questa pagina viene presentato un approccio generale a queste primitive. Il framework scelto potrebbe avere un aspetto simile a questo, ma non si tratta di un riferimento esatto.

Ad esempio:

import { fibonacci, catalan } from '../src/math.js';
import { assert, test, suite } from 'a-made-up-testing-library';

suite('math tests', () => {
  test('fibonacci function', () => {
    // check expected fibonacci numbers against our known actual values
    // with an explanation if the values don't match
    assert.equal(fibonacci(0), 0, 'Invalid 0th fibonacci result');
    assert.equal(fibonacci(13), 233, 'Invalid 13th fibonacci result');
  });
  test('relationship between sequences', () => {
    // catalan numbers are greater than fibonacci numbers (but not equal)
    assert.isAbove(catalan(4), fibonacci(4));
  });
  test('bugfix: check bug #4141', () => {
    assert.isFinite(fibonacci(0)); // fibonacci(0) was returning NaN
  })
});

Questo esempio crea un gruppo di test (a volte chiamato suite) chiamato "test matematici" e definisce tre scenari di test indipendenti, ciascuno dei quali esegue alcune asserzioni. Questi scenari di test possono in genere essere gestiti o eseguiti singolarmente, ad esempio, da un flag di filtro nell'esecutore di test.

Gli assistenti alle asserzioni come primitive

La maggior parte dei framework di test, incluso Vitest, include una raccolta di aiutanti delle asserzioni su un oggetto assert che consentono di verificare rapidamente i valori restituiti o altri stati rispetto ad alcune expectation. Tale aspettativa è spesso un valore noto. Nell'esempio precedente, sappiamo che il tredicesimo numero di Fibonacci deve essere 233, quindi possiamo confermarlo utilizzando direttamente assert.equal.

Potresti anche avere l'aspettativa che un valore assuma una certa forma, sia maggiore di un altro valore o abbia un'altra proprietà. Questo corso non coprirà l'intera gamma di possibili aiutanti per le asserzioni, ma i framework di test forniscono sempre almeno i seguenti controlli di base:

  • Un controllo di autenticità, spesso descritto come un controllo "ok", verifica che una condizione sia vera e corrisponde al modo in cui potresti scrivere un if che verifica se qualcosa è andato a buon fine o è corretto. Viene generalmente fornito come assert(...) o assert.ok(...) e prevede un singolo valore più un commento facoltativo.

  • Un controllo di uguaglianza, come nell'esempio di un test matematico, in cui prevedi che il valore restituito o lo stato di un oggetto corrisponda a un valore noto. Questi sono per l'uguaglianza primitiva (ad esempio per numeri e stringhe) o per l'uguaglianza referenziale (si tratta dello stesso oggetto). In dettaglio, questi sono solo un controllo "di verità" con un confronto tra == o ===.

    • JavaScript fa distinzione tra uguaglianza debole (==) e rigorosa (===). La maggior parte delle librerie di test fornisce i metodi assert.equal e assert.strictEqual, rispettivamente.
  • Controlli profondi di uguaglianza, che estendono i controlli di uguaglianza per includere il controllo dei contenuti di oggetti, array e altri tipi di dati più complessi, nonché della logica interna per il trasferimento degli oggetti per il confronto. Questi aspetti sono importanti perché JavaScript non ha un modo integrato per confrontare i contenuti di due oggetti o array. Ad esempio, [1,2,3] == [1,2,3] è sempre false. I framework di test spesso includono helper deepEqual o deepStrictEqual.

Gli assistenti per le asserzioni che confrontano due valori (anziché il solo controllo di verità) richiedono in genere due o tre argomenti:

  • Il valore effettivo, generato dal codice in fase di test o descrizione dello stato da convalidare.
  • Il valore previsto, generalmente hardcoded (ad esempio un numero letterale o una stringa).
  • Commento facoltativo che descrive cosa previsto o cosa non è riuscito, che verrà incluso se il testo non funziona.

È inoltre abbastanza comune combinare le asserzioni per creare una varietà di controlli, poiché è raro che sia in grado di confermare correttamente lo stato del sistema da solo. Ad esempio:

  test('JWT parse', () => {
    const json = decodeJwt('eyJieSI6InNhbXRob3Ii…');

    assert.ok(json.payload.admin, 'user should be admin');
    assert.deepEqual(json.payload.groups, ['role:Admin', 'role:Submitter']);
    assert.equal(json.header.alg, 'RS265')
    assert.isAbove(json.payload.exp, +new Date(), 'expiry must be in future')
  });

Vitest utilizza internamente la libreria delle asserzioni Chai per fornire i suoi assistenti per le asserzioni; può essere utile esaminare i riferimenti per capire quali asserzioni e assistenti potrebbero essere adatti al tuo codice.

Affermazioni Fluent e BDD

Alcuni sviluppatori preferiscono uno stile di asserzione che può essere chiamato sviluppo basato sul comportamento (BDD) o asserzioni di tipo Fluent. Questi sono chiamati anche aiutanti "aspettati", perché il punto di ingresso per la verifica delle aspettative è un metodo denominato expect().

Aspettati che gli helper si comportino come le asserzioni scritte come semplici chiamate di metodi come assert.ok o assert.strictDeepEquals, ma alcuni sviluppatori le trovano più facili da leggere. Un'asserzione BDD potrebbe essere simile al seguente:

// A failure here would generate "Expect result to be an array that does include 42"
const result = await possibleMeaningsOfLife();
expect(result).to.be.an('array').that.does.include(42);

// or a simpler form
expect(result).toBe('array').toContainEqual(42);

// the same in assert might be
assert.typeOf(result, 'array', 'Expected the result to be an array');
assert.include(result, 42, 'Expected the result to include 42');

Questo stile di asserzioni funziona grazie a una tecnica chiamata concatenamento dei metodi, in cui l'oggetto restituito da expect può essere continuamente concatenato ad altre chiamate al metodo. Alcune parti della chiamata, incluse to.be e that.does nell'esempio precedente, non hanno funzione e sono incluse solo per rendere la chiamata più facile da leggere e potenzialmente generare un commento automatico se il test non è riuscito. In particolare, expect normalmente non supporta un commento facoltativo, perché il concatenamento dovrebbe descrivere chiaramente l'errore.

Molti framework di test supportano sia Fluent/BDD che asserzioni regolari. Vitest, ad esempio, esporta entrambi gli approcci di Chai e ha un proprio approccio leggermente più conciso al BDD. Jest, d'altra parte, include solo un metodo previsto per impostazione predefinita.

Raggruppa i test tra i file

Quando scrivi i test, tendiamo già a fornire raggruppamenti impliciti, piuttosto che tutti i test si trovano in un unico file, è comune scrivere test su più file. Infatti, gli esecutori del test di solito sanno che un file è per il test grazie a un filtro predefinito o a un'espressione regolare; vitest, ad esempio, include tutti i file nel progetto che terminano con un'estensione come ".test.jsx" o ".spec.ts" (".test" e ".spec" più un numero di estensioni valide).

I test dei componenti tendono a trovarsi in un file peer per il componente in fase di test, come nella seguente struttura di directory:

Un elenco di file in una directory, tra cui UserList.tsx e UserList.test.tsx.
Un file del componente e il relativo file di test.

Analogamente, i test delle unità tendono a essere posizionati accanto al codice in fase di test. I test end-to-end possono essere inseriti nel proprio file e i test di integrazione possono anche essere inseriti in cartelle specifiche. Queste strutture possono essere utili quando gli scenari di test complessi aumentano fino a richiedere i propri file di supporto non di test, ad esempio le librerie di supporto necessarie solo per un test.

Raggruppa i test all'interno dei file

Come utilizzato negli esempi precedenti, è pratica comune inserire i test all'interno di una chiamata a suite() che raggruppa i test che hai configurato con test(). Le suite di solito non vengono testate autonomamente, ma aiutano a fornire una struttura raggruppando test o obiettivi correlati chiamando il metodo Superato. Per test(), il metodo superato descrive le azioni del test stesso.

Come per le affermazioni, in Fluent/BDD esiste un'equivalenza piuttosto standard ai test di raggruppamento. Alcuni esempi tipici vengono confrontati nel codice seguente:

// traditional/TDD
suite('math tests', () => {
  test('handle zero values', () => {
    assert.equal(fibonacci(0), 0);
  });
});

// Fluent/BDD
describe('math tests', () => {
  it('should handle zero values', () => {
    expect(fibonacci(0)).toBe(0);
  });
})

Nella maggior parte dei framework, suite e describe si comportano in modo simile, così come test e it, a differenza delle maggiori differenze tra l'uso di expect e assert per scrivere le asserzioni.

Altri strumenti hanno approcci leggermente diversi all'organizzazione di suite e test. Ad esempio, l'esecutore di test integrato di Node.js supporta chiamate nidificate a test() per creare implicitamente una gerarchia di test. Tuttavia, Vitest consente questo tipo di nidificazione solo tramite suite() e non eseguirà un test() definito all'interno di un altro test().

Come per le asserzioni, ricorda che la combinazione esatta di metodi di raggruppamento forniti dal tuo stack tecnico non è così importante. Questo corso li tratterà in astratto, ma devi capire come si applicano alla tua scelta di strumenti.

Metodi del ciclo di vita

Un motivo per raggruppare i test, anche implicitamente al livello più alto di un file, è fornire metodi di configurazione e rimozione da eseguire per ogni test o una volta per un gruppo di test. La maggior parte dei framework offre quattro metodi:

Per ogni "test()" o "it()" Una volta per la suite
Prima delle esecuzioni del test "beforeCiascun()" "beforeAll()"
Dopo le esecuzioni di test "dopoCiascun()" "afterAll()"

Ad esempio, potresti voler precompilare un database utenti virtuale prima di ogni test e cancellarlo dopo:

suite('user test', () => {
  beforeEach(() => {
    insertFakeUser('bob@example.com', 'hunter2');
  });
  afterEach(() => {
    clearAllUsers();
  });

  test('bob can login', async () => { … });
  test('alice can message bob', async () => { … });
});

Questo può essere utile per semplificare i test. Puoi condividere il codice di configurazione e rimozione comune, anziché duplicarlo in ogni test. Inoltre, se il codice di configurazione e di eliminazione genera un errore, ciò può indicare problemi strutturali che non implicano l'esito negativo dei test.

Indicazioni generali

Ecco alcuni suggerimenti da ricordare quando si pensa a queste primitive.

I primitivi sono una guida

Ricorda che gli strumenti e le primitive riportati qui e nelle prossime pagine non corrisponderanno esattamente a Vitest, Jest, Mocha, Web Test Runner o a qualsiasi altro framework specifico. Anche se abbiamo usato Vitest come guida generale, assicurati di associarli al framework che preferisci.

Combina e associa le asserzioni in base alle tue esigenze

I test sono fondamentalmente codice che può generare errori. Ogni runner fornirà un valore primitivo, probabilmente test(), per descrivere scenari di test distinti.

Tuttavia, se questo runner fornisce anche assert(), expect() e aiutanti per l'asserzione, ricorda che questa parte riguarda più la comodità e puoi saltarla se è necessario. Puoi eseguire qualsiasi codice che potrebbe generare un errore, incluse altre librerie di assertion o un'istruzione if obsoleta.

La configurazione IDE può essere una salvezza

Garantire che l'IDE, come VSCode, abbia accesso al completamento automatico e alla documentazione sugli strumenti di test scelti può aumentare la produttività. Ad esempio, esistono oltre 100 metodi su assert nella libreria delle asserzioni Chai e avere la documentazione per quello giusto sembra in linea può essere conveniente.

Questo può essere particolarmente importante per alcuni framework di test che popolano lo spazio dei nomi globale con i propri metodi di test. Si tratta di una piccola differenza, ma spesso è possibile utilizzare le librerie di test senza importarle se vengono aggiunte automaticamente allo spazio dei nomi globale:

// some.test.js
test('using test as a global', () => { … });

Consigliamo di importare gli helper anche se sono supportati automaticamente, poiché in questo modo il tuo IDE ha un modo chiaro per cercare questi metodi. (Potresti aver riscontrato questo problema durante la creazione di React, poiché alcuni codebase hanno un React magico globale, ma altri no, e altri no, e questo richiede l'importazione in tutti i file utilizzando React.)

// some.test.js
import { test } from 'vitest';
test('using test as an import', () => { … });