Che cos'è il test

Quando scrivi software, puoi confermare che funzioni correttamente tramite test. Per test si intende in generale il processo di esecuzione di un software in modi specifici per garantire che si comporti come previsto.

I test riusciti ti consentono di avere la certezza che, man mano che aggiungi nuovo codice o nuove funzionalità, o addirittura esegui l'upgrade delle dipendenze, il software che hai già scritto continuerà a funzionare nel modo previsto. I test possono anche contribuire a salvaguardare il software da scenari improbabili o input imprevisti.

Ecco alcuni esempi di comportamenti sul web che ti consigliamo di verificare:

  • Assicurarsi che la funzionalità di un sito web funzioni correttamente quando viene fatto clic su un pulsante.
  • La conferma che una funzione complessa produce i risultati corretti.
  • Completamento di un'azione che richiede l'accesso da parte dell'utente.
  • Verificare che un modulo segnali correttamente un errore quando vengono inseriti dati in formato non corretto.
  • Garantire che un'app web complessa continui a funzionare quando un utente ha una larghezza di banda estremamente bassa o non è in linea.

Test automatici e manuali

Puoi testare il tuo software in due modi generali: test automatici e test manuali.

I test manuali prevedono l'esecuzione diretta di software da parte di persone fisiche, ad esempio il caricamento di un sito web nel browser e la conferma che il software funzioni come previsto. I test manuali sono semplici da creare o definire. Ad esempio, il sito può essere caricato? Riesci a eseguire queste azioni? Ma ogni run-through costa un'enorme quantità di tempo a un essere umano. Anche se gli esseri umani sono molto creativi, il che può consentire un tipo di test noto come test esplorativo, possiamo comunque fare fatica a notare errori o incongruenze, soprattutto se ripetiamo la stessa attività molte volte.

Per test automatico si intende qualsiasi processo che consente di codificare i test ed eseguirli ripetutamente da un computer per confermare il comportamento previsto del software senza che un essere umano esegua passaggi ripetuti, come la configurazione o il controllo dei risultati. È importante sottolineare che, una volta configurato, i test automatici possono essere eseguiti di frequente. Questa è ancora una definizione molto ampia e vale la pena notare che i test automatizzati assumono ogni tipo di elemento. La maggior parte di questo corso riguarda i test automatici come pratica.

I test manuali hanno il loro posto, spesso come precursori della scrittura di test automatizzati, ma anche quando questi ultimi diventano troppo inaffidabili, di portata ampia o difficili da usare per la scrittura.

Concetti fondamentali con un esempio

Per noi, in qualità di sviluppatori web che scrivono JavaScript o in linguaggi correlati, un test automatico conciso potrebbe essere uno script simile a questo che esegui tutti i giorni, magari tramite Node o caricandolo in un browser:

import { fibonacci } from "../src/math.js";

if (fibonacci(0) !== 0) {
  throw new Error("Invalid 0th fibonacci result");
}
const fib13 = fibonacci(13);
if (fib13 !== 233) {
  throw new Error("Invalid 13th fibonacci result, was=${fib13} wanted=233");
}

Questo è un esempio semplificato che fornisce i seguenti insight:

  • Questo è un test perché esegue alcuni software (la funzione Fibonacci) e garantisce che il suo comportamento funzioni come previsto, verificando i risultati rispetto ai valori previsti. Se il comportamento non è corretto, causa un errore che JavaScript si esprime generando un Error.

  • Anche se esegui questo script manualmente nel tuo terminale o in un browser, si tratta comunque di un test automatico, perché può essere eseguito ripetutamente senza che tu debba eseguire singoli passaggi. Nella pagina successiva, dove vengono eseguiti i test, viene fornita una spiegazione più dettagliata.

  • Anche se questo test non utilizza librerie (è JavaScript che può essere eseguito ovunque), è comunque un test. Esistono molti strumenti che possono aiutarti a scrivere test, inclusi quelli che verranno illustrati più avanti in questo corso, ma tutti si basano sul principio fondamentale che prevede la generazione di un errore in caso di problemi.

Test delle librerie nella pratica

La maggior parte delle librerie o dei framework di test integrati fornisce due primitive principali che rendono i test più facili da scrivere: le assertions e un modo per definire test indipendenti. Questi aspetti saranno trattati in dettaglio nella prossima sezione, asserzioni e altre primitive. Tuttavia, a livello generale, è importante ricordare che quasi tutti i test che vedi o scrivi finiscono per utilizzare questo tipo di primitive.

Le asserzioni sono un modo per combinare il controllo di un risultato e generare un errore se si verifica un problema. Ad esempio, puoi rendere il test precedente più conciso introducendo assert:

import { fibonacci } from "../src/math.js";
import { assert } from "a-made-up-testing-library";

assert.equal(fibonacci(0), 0, "Invalid 0th fibonacci result");
assert.equal(fibonacci(13), 233, "Invalid 13th fibonacci result");

Puoi migliorare ulteriormente questo test definendo test indipendenti, facoltativamente raggruppati in suite. La suite seguente testa in modo indipendente la funzione di Fibonacci e la funzione Catalano:

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

suite("math tests", () => {
  test("fibonacci function", () => {
    assert.equal(fibonacci(0), 0, "Invalid 0th fibonacci result");
    assert.equal(fibonacci(13), 233, "Invalid 13th fibonacci result");
  });
  test("relationship between sequences", () => {
    const numberToCheck = 4;
    const fib = fibonacci(numberToCheck);
    const cat = catalan(numberToCheck);
    assert.isAbove(fib, cat);
  });
});

In questo contesto dei test del software, il termine test come sostantivo si riferisce a uno scenario di test: un singolo scenario di test indipendente e indirizzabile, come lo scenario di test "relazione tra sequenze" nell'esempio precedente.

I test denominati singolarmente sono utili per le seguenti attività, tra le altre:

  • Determinare il modo in cui un test ha esito positivo o negativo nel tempo.
  • Evidenziazione di un bug o di uno scenario in base al nome, in modo da poter verificare più facilmente che lo scenario è stato risolto.
  • Esecuzione di alcuni test in modo indipendente da altri, ad esempio tramite un filtro glob.

Un modo per pensare agli scenari di test è usare le "tre A" del test delle unità: disponi, agisci e afferma. In sostanza, ogni scenario di test:

  • Disponi alcuni valori o stati (potrebbero trattarsi solo di dati di input hardcoded).
  • Eseguire un'azione, ad esempio chiamare un metodo.
  • Dichiara i valori di output o lo stato aggiornato (utilizzando assert).

La portata dei test

Gli esempi di codice nella sezione precedente descrivono un test delle unità, in quanto testano parti minori del software, spesso concentrate su un singolo file e, in questo caso, solo l'output di una singola funzione. La complessità dei test aumenta man mano che consideri il codice proveniente da più file, componenti o anche diversi sistemi interconnessi (a volte al di fuori del tuo controllo, come un servizio di rete o il comportamento di una dipendenza esterna). Per questo motivo, i tipi di test vengono spesso denominati in base all'ambito o alla scala.

Insieme ai test delle unità, alcuni esempi di altri tipi di test includono i test dei componenti, i test visivi e i test di integrazione. Nessuno di questi nomi ha definizioni rigorose e potrebbe avere significati diversi a seconda del codebase, quindi ricordati di usarli come guida e inventa definizioni che funzionino per te. Ad esempio, qual è un componente in fase di test nel tuo sistema? Per gli sviluppatori di React, potrebbe essere mappato letteralmente a un "componente React", ma potrebbe avere un significato diverso per gli sviluppatori in altri contesti.

La portata di un singolo test può collocarlo all'interno di un concetto spesso definito "piramide di test", che può essere una buona regola generale per determinare ciò che viene verificato da un test e come funziona.

La piramide di test, con i test end-to-end (E2E) nella parte superiore, i test di integrazione a metà e i test delle unità nella parte inferiore.
La piramide sperimentale.

Questa idea è stata perfezionata e ora sono state popolarizzate altre forme, come il diamante di prova o il cono di ghiaccio di prova. Probabilmente le priorità di scrittura dei test sono specifiche del tuo codebase. Tuttavia, una caratteristica comune è che i test più semplici, come i test delle unità, tendono a essere più veloci da eseguire, più facili da scrivere (quindi ne avrai di più) e a testare un ambito limitato, mentre i test complessi come i test end-to-end sono difficili da scrivere ma possono testare un ambito più ampio. Infatti, il livello principale di molte "forme" di test tende a essere il test manuale, perché alcune interazioni dell'utente sono troppo complesse per essere codificate in un test automatico.

Questi tipi di test verranno estesi nei tipi di test automatici.

Verifica le tue conoscenze

Quali primitive fornisce la maggior parte delle librerie e dei framework di test?

Un servizio runner che utilizza un cloud provider.
Alcuni runner basati su browser offrono un modo per eseguire l'outsourcing dei test, ma non è una funzionalità normale delle librerie di test.
Dichiarazioni che causano eccezioni se non vengono soddisfatte.
Sebbene sia possibile generare un errore per non superare un test, assert() e le sue varianti tendono a essere inclusi perché semplificano la scrittura dei controlli.
Un modo per classificare i test nella piramide di test.
Non esiste un modo standard per farlo. Puoi aggiungere un prefisso ai nomi dei test o inserirli in file diversi, ma la categorizzazione non è realmente integrata nella maggior parte dei framework di test.
La capacità di definire test indipendenti per funzione.
Il metodo test() è incluso in quasi tutti i test runner. È importante perché il codice del test non viene eseguito al primo livello di un file, consentendo all'esecutore del test di trattare ogni scenario di test come un'unità indipendente.