Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Organizzare i Test

Come accennato all’inizio del capitolo, i test sono una disciplina complessa e diverse persone tendono ad utilizzare una terminologia e un’organizzazione diverse. La comunità di Rust pensa ai test in termini di due categorie principali: i test unitari (unit test) e i test di integrazione (integration test). Gli unit test sono piccoli e più mirati, testano un singolo modulo alla volta e possono testare interfacce private. Gli integration test sono invece esterni alla tua libreria e utilizzano il tuo codice nello stesso modo in cui lo farebbe qualsiasi altro codice esterno, utilizzando solo l’interfaccia pubblica e potenzialmente utilizzando più moduli per test.

Scrivere entrambi i tipi di test è importante per garantire che i pezzi della tua libreria facciano ciò che ti aspetti, sia separatamente che quando integrate in altro codice.

Test Unitari

Lo scopo degli unit test è quello di testare ogni unità di codice in modo isolato dal resto del codice per individuare rapidamente i punti in cui il codice funziona e non funziona come previsto. Gli unit test vengono inseriti nella cartella src in ogni file con il codice che stanno testando. La convenzione è quella di creare un modulo chiamato tests in ogni file per contenere le funzioni di test e di annotare il modulo con cfg(test).

Il Modulo tests e #[cfg(test)]

L’annotazione #[cfg(test)] sul modulo tests dice a Rust di compilare ed eseguire il codice di test solo quando si esegue cargo test, non quando si esegue cargo build. In questo modo si risparmia tempo di compilazione quando si vuole costruire solo la libreria e si risparmia spazio nell’artefatto compilato risultante perché i test non sono inclusi. Vedrai che, poiché i test di integrazione si trovano in una cartella diversa, non hanno bisogno dell’annotazione #[cfg(test)]. Tuttavia, poiché gli unit test si trovano negli stessi file del codice, dovrai specificare #[cfg(test)] per evitare che siano inclusi nel risultato compilato.

Ricordi che quando abbiamo generato il nuovo progetto addizione nella prima sezione di questo capitolo, Cargo ha generato questo codice per noi:

File: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

Nel modulo tests generato automaticamente, l’attributo cfg sta per configuration (configurazione) e indica a Rust che il seguente elemento deve essere incluso solo in presenza di una determinata opzione di configurazione. In questo caso, l’opzione di configurazione è test, che viene fornita da Rust per la compilazione e l’esecuzione dei test. Utilizzando l’attributo cfg, Cargo compila il nostro codice di test solo se effettivamente eseguiamo i test con cargo test. Questo include qualsiasi funzione ausiliaria che potrebbero essere presente in questo modulo, oltre alle funzioni annotate con #[test].

Testare Funzioni Private

All’interno della comunità dei tester si discute se le funzioni private debbano essere testate direttamente o meno, e altri linguaggi rendono difficile o impossibile testare le funzioni private. Indipendentemente dall’ideologia di testing a cui aderisci, le regole sulla privacy di Rust ti permettono di testare le funzioni private. Considera il codice nel Listato 11-12 con la funzione privata addizione_privata.

File: src/lib.rs
pub fn aggiungi_due(a: u64) -> u64 {
    addizione_privata(a, 2)
}

fn addizione_privata(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn privata() {
        let result = addizione_privata(2, 2);
        assert_eq!(result, 4);
    }
}
Listato 11-12: Test di una funzione privata

Nota che la funzione addizione_privata non è contrassegnata come pub. I test sono solo codice Rust e il modulo tests è solo un altro modulo. Come abbiamo discusso in “Percorsi per Fare Riferimento a un Elemento nell’Albero dei Moduli”, gli elementi dei moduli figli possono utilizzare gli elementi dei loro moduli antenati. In questo test, portiamo tutti gli elementi dei moduli genitore del modulo tests nello scope con use super::*, e poi il test può chiamare addizione_privata. Se non pensi che le funzioni private debbano essere testate, non c’è nulla in Rust che ti costringa a farlo.

Test di Integrazione

In Rust, i test di integrazione sono esterni alla tua libreria. Utilizzano la libreria nello stesso modo in cui lo farebbe qualsiasi altro codice, il che significa che possono chiamare solo le funzioni che fanno parte dell’API pubblica della libreria. Il loro scopo è quello di verificare se molte parti della libreria funzionano correttamente insieme. Unità di codice che funzionano correttamente da sole potrebbero avere problemi quando vengono integrate, quindi creare test che verifichino queste funzionalità del codice integrato è importante. Per creare i test di integrazione, hai bisogno innanzitutto di una cartella tests.

La Cartella tests

Creiamo una cartella tests all’inizio della nostra cartella di progetto, accanto a src. Cargo sa che deve cercare i file di test di integrazione in questa cartella. Possiamo quindi creare tutti i file di test che vogliamo e Cargo compilerà ciascuno di essi come crate separati.

Creiamo un test di integrazione. Con il codice del Listato 11-12 ancora nel file src/lib.rs, crea una cartella tests e un nuovo file chiamato tests/test_integrazione.rs. La struttura delle cartelle del tuo progetto dovrebbe essere simile a questa:

addizione
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── test_integrazione.rs

Inserisci il codice del Listato 11-13 nel file tests/test_integrazione.rs.

File: tests/test_integrazione.rs
use addizione::aggiungi_due;

#[test]
fn aggiungere_due() {
    let risultato = aggiungi_due(2);
    assert_eq!(risultato, 4);
}
Listato 11-13: Un test di integrazione di una funzione nel crate addizione

Ogni file della cartella tests è un crate separato, quindi dobbiamo portare la nostra libreria nello scope di ogni crate di test. Per questo motivo aggiungiamo use addizione::aggiungi_due; all’inizio del codice, che non era necessario negli unit test usati finora.

Non abbiamo bisogno di annotare alcun codice in tests/test_integrazione.rs con #[cfg(test)]. Cargo tratta la cartella tests in modo speciale e compila i file in questa cartella solo quando eseguiamo cargo test. Esegui ora cargo test:

$ cargo test
   Compiling addizione v0.1.0 (file:///progetti/addizione)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.80s
     Running unittests src/lib.rs (target/debug/deps/addizione-41054392f08da196)

running 1 test
test tests::privata ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/test_integrazione.rs (target/debug/deps/test_integrazione-a2e6a22ac01f911a)

running 1 test
test aggiungere_due ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests addizione

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Le tre sezioni di output comprendono gli unit test, i test di integrazione e i test di documentazione. Nota che se un test di una sezione fallisce, le sezioni successive non verranno eseguite. Ad esempio, se un unit test fallisce, non ci sarà alcun output per i test di integrazione e di documentazione perché questi test verranno eseguiti solo se tutti gli unit test passano.

La prima sezione per gli unit test è la stessa che abbiamo visto finora: una riga per ogni unit test (una denominata privata che abbiamo aggiunto nel Listato 11-12) e poi una riga di riepilogo per i unit test.

La sezione dei test di integrazione inizia con la riga Running test/test_integrazione.rs. Poi, c’è una riga per ogni funzione di test in quel test di integrazione e una riga di riepilogo dei risultati del test di integrazione appena prima dell’inizio della sezione Doc-tests addizione.

Ogni file di test di integrazione ha una propria sezione, quindi se aggiungiamo altri file nella cartella tests, ci saranno più sezioni di test di integrazione.

Possiamo comunque eseguire una particolare funzione di test di integrazione specificando il nome della funzione di test come argomento di cargo test. Per eseguire tutti i test in un particolare file di test di integrazione, usa l’argomento --test di cargo test seguito dal nome del file:

$ cargo test --test test_integrazione
   Compiling addizione v0.1.0 (file:///progetti/addizione)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
     Running tests/test_integrazione.rs (target/debug/deps/test_integrazione-a2e6a22ac01f911a)

running 1 test
test aggiungere_due ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Questo comando esegue solo i test presenti nel file tests/test_integrazione.rs.

Sottomoduli nei Test di Integrazione

Man mano che aggiungi altri test di integrazione, potresti voler creare altri file nella cartella tests per organizzarli; ad esempio, puoi raggruppare le funzioni di test in base alla funzionalità che stanno testando. Come già detto, ogni file nella cartella tests viene compilato come un proprio crate separato, il che è utile per creare scope separati per imitare il più possibile il modo in cui gli utenti finali utilizzeranno il tuo crate. Tuttavia, questo significa che i file nella cartella tests non condividono lo stesso comportamento dei file in src, come hai appreso nel Capitolo 7 su come separare il codice in moduli e file.

Il diverso comportamento dei file della cartella tests si nota soprattutto quando hai una serie di funzioni comuni da utilizzare in più file di test di integrazione e cerchi di seguire i passi della sezione “Separare i Moduli in File Diversi” del Capitolo 7 per metterle in un modulo comune. Ad esempio, se creiamo tests/comune.rs e vi inseriamo una funzione chiamata inizializzazione a cui aggiungere del codice che vogliamo chiamare da più funzioni di test in più file di test:

File: tests/comune.rs

pub fn inizializzazione() {
    // codice specifico di inizializzazione della libreria
}

Quando eseguiamo nuovamente i test, vedremo una nuova sezione nell’output del test per il file comune.rs, anche se questo file non contiene alcuna funzione di test né abbiamo chiamato la funzione inizializzazione da nessuna parte:

$ cargo test
   Compiling addizione v0.1.0 (file:///progetti/addizione)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.92s
     Running unittests src/lib.rs (target/debug/deps/addizione-41054392f08da196)

running 1 test
test tests::privata ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/comune.rs (target/debug/deps/comune-9acf22d6dcb0de0a)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/test_integrazione.rs (target/debug/deps/test_integrazione-a2e6a22ac01f911a)

running 1 test
test aggiungere_due ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests addizione

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Il fatto che comune appaia nei risultati dei test con running 0 tests (eseguiti 0 test) non è quello che volevamo. Volevamo solo condividere un po’ di codice con gli altri file dei test di integrazione. Per evitare che comune appaia nell’output dei test, invece di creare tests/comune.rs, creeremo tests/comune/mod.rs. La cartella del progetto ora ha questo aspetto:

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    ├── comune
    │   └── mod.rs
    └── test_integrazione.rs

Questa è la vecchia convenzione di denominazione comunque compresa da Rust, di cui abbiamo parlato in “Percorsi di File Alternativi” nel Capitolo 7. Nominare il file in questo modo indica a Rust di non trattare il modulo comune come un file di test di integrazione. Quando spostiamo il codice della funzione inizializzazione in tests/comune/mod.rs e cancelliamo il file tests/comune.rs, la sezione nell’output del test non apparirà più. I file nelle sottocartelle della cartella tests non vengono compilati come crate separati né hanno sezioni nell’output del test.

Dopo aver creato tests/comune/mod.rs, possiamo utilizzarlo da qualsiasi file di test di integrazione come modulo. Ecco un esempio di chiamata della funzione inizializzazione dal test aggiungere_due in tests/test_integrazione.rs:

File: tests/test_integrazione.rs

use addizione::aggiungi_due;

mod comune;

#[test]
fn aggiungere_due() {
    comune::inizializzazione();

    let risultato = aggiungi_due(2);
    assert_eq!(risultato, 4);
}

Nota come la dichiarazione mod comune; è uguale alla dichiarazione che abbiamo mostrato nel Listato 7-21. Quindi, nella funzione di test, possiamo chiamare la funzione comune::inizializzazione().

Test di Integrazione per i Crate Binari

Se il nostro progetto è un crate binario che contiene solo un file src/main.rs e non ha un file src/lib.rs, non possiamo creare test di integrazione nella cartella tests e testare le funzioni definite nel file src/main.rs con una dichiarazione use. Solo i crate libreria espongono funzioni che altri crate possono utilizzare; i crate binari sono pensati per essere eseguiti da soli.

Per questo è buona pratica per i progetti Rust che forniscono un binario avere un file src/main.rs semplice che si limita a richiamare la logica che risiede nel file src/lib.rs. Utilizzando questa struttura, i test di integrazione possono testare il crate libreria con use per rendere disponibile le funzionalità che ci interessa testare. Se la funzionalità passa il test, anche la piccola quantità di codice nel file src/main.rs funzionerà e quella piccola quantità di codice non dovrà essere testata.

Le funzionalità di testing di Rust forniscono un modo per specificare come il codice debba funzionare e ci si assicuri che continui a funzionare come ci si aspetta, anche quando si apportano delle modifiche. I test unitari usano e testano le diverse parti di una libreria separatamente e possono testare i dettagli privati dell’implementazione. I test di integrazione verificano che molte parti della libreria funzionino insieme correttamente e utilizzano l’API pubblica della libreria per testare il codice nello stesso modo in cui lo utilizzerà il codice esterno. Anche se il sistema dei type e le regole di ownership di Rust aiutano a prevenire alcuni tipi di bug, i test sono comunque importanti per ridurre i bug logici che hanno a che fare con il modo in cui ci si aspetta che il codice si comporti.

Combiniamo le conoscenze apprese in questo capitolo e nei capitoli precedenti per lavorare a un progetto!