SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI
Organizzazione dei Test
Come accennato all’inizio del capitolo, il testing è una disciplina complessa e diverse persone utilizzano diverse terminologie e organizzazioni. La comunità Rust pensa ai test in termini di due categorie principali: test unitari e test di integrazione. I test unitari sono piccoli e più focalizzati, testano un modulo alla volta in isolamento e possono testare interfacce private. I test di integrazione sono completamente esterni alla tua libreria e utilizzano il tuo codice allo stesso modo di qualsiasi altro codice esterno, utilizzando solo l’interfaccia pubblica e potenzialmente eseguendo più moduli per test.
Scrivere entrambi i tipi di test è importante per assicurarsi che i pezzi della tua libreria facciano quello che ti aspetti, separatamente e insieme.
Test Unitari
Lo scopo dei test unitari è testare ogni unità di codice in isolamento dal resto del codice per individuare rapidamente dove il codice funziona e dove no. Metterai i test unitari nella directory src in ogni file con il codice che stanno testando. La convenzione è quella di creare un modulo denominato tests in ogni file per contenere le funzioni di test e di annotare il modulo con cfg(test).
Il modulo dei test e #[cfg(test)]
L’annotazione #[cfg(test)] sul modulo dei test dice a Rust di compilare ed eseguire il codice di test solo quando si esegue cargo test, non quando si esegue cargo build. Ciò risparmia tempo di compilazione quando si desidera solo compilare la libreria e risparmia spazio nell’artefatto compilato risultante perché i test non sono inclusi. Vedrai che poiché i test di integrazione vanno in una directory diversa, non è necessaria l’annotazione #[cfg(test)]. Tuttavia, poiché i test unitari vanno negli stessi file del codice, userai #[cfg(test)] per specificare che non devono essere inclusi nel risultato compilato.
Ricorda che quando abbiamo generato il nuovo progetto adder nella prima sezione di questo capitolo, Cargo ha generato questo codice per noi:
rust
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
Questo codice è il modulo di test generato automaticamente. L’attributo cfg sta per configurazione e dice a Rust che l’elemento seguente dovrebbe essere incluso solo data una certa opzione di configurazione. In questo caso, l’opzione di configurazione è test, che è fornita da Rust per compilare ed eseguire i test. Utilizzando l’attributo cfg, Cargo compila il nostro codice di test solo se eseguiamo attivamente i test con cargo test. Questo include eventuali funzioni helper che potrebbero essere all’interno di questo modulo, oltre alle funzioni annotate con #[test].
Testing delle Funzioni Private
C’è un dibattito all’interno della comunità dei test su se le funzioni private dovrebbero essere testate direttamente, e altri linguaggi rendono difficile o impossibile testare le funzioni private. Indipendentemente da quale ideologia di testing segui, le regole sulla privacy di Rust ti consentono di testare le funzioni private. Considera il codice nella Lista 11-12 con la funzione private internal_adder.
rust
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
} fn internal_adder(a: i32, b: i32) -> i32 {
a + b
} #[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}
Nota che la funzione internal_adder non è contrassegnata come pub. I test sono solo codice Rust, e il modulo dei test è solo un altro modulo. Come discusso nella sezione “Percorsi per fare riferimento a un elemento nell’albero dei moduli”, gli elementi nei moduli figlio possono utilizzare gli elementi nei loro moduli genitori. In questo test, portiamo tutti gli elementi del genitore del modulo dei test nello scope con use super::* e quindi il test può chiamare internal_adder. Se non pensi che le funzioni private dovrebbero essere testate, non c’è nulla in Rust che ti costringerà a farlo.
Test di Integrazione
In Rust, i test di integrazione sono completamente esterni alla tua libreria. Utilizzano la tua libreria allo stesso modo di qualsiasi altro codice, il che significa che possono chiamare solo le funzioni che fanno parte dell’API pubblica della tua libreria. Il loro scopo è testare se molte parti della tua libreria funzionano correttamente insieme. Le unità di codice che funzionano correttamente da sole potrebbero avere problemi quando integrate, quindi è importante anche la copertura dei test del codice integrato. Per creare test di integrazione, prima è necessaria una directory di test.
La Directory dei Test
Creiamo una directory di test nella directory principale del nostro progetto, accanto a src. Cargo sa di cercare file di test di integrazione in questa directory. Possiamo quindi creare tanti file di test quanti ne vogliamo, e Cargo compilerà ciascun file come un crate individuale.
Creiamo un test di integrazione. Con il codice nella Lista 11-12 ancora nel file src/lib.rs, creiamo una directory dei test e creiamo un nuovo file denominato tests/integration_test.rs. La tua struttura della directory dovrebbe assomigliare a questa:
csharp
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
Inserisci il codice nella Lista 11-13 nel file tests/integration_test.rs:
rust
use adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
Ogni file nella directory dei test è un crate separato, quindi dobbiamo portare la nostra libreria nello scope di ciascun crate di test. Per questo motivo aggiungiamo use adder all’inizio del codice, cosa che non era necessaria nei test unitari.
Non è necessario annotare alcun codice in tests/integration_test.rs con #[cfg(test)]. Cargo tratta la directory dei test in modo speciale e compila i file in questa directory solo quando eseguiamo cargo test. Esegui ora cargo test:
sh
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 1.31s
Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6) running 1 test
test tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6) running 1 test
test it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder 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 includono i test unitari, il test di integrazione e i test doc. Nota che se un test in una sezione fallisce, le sezioni successive non verranno eseguite. Ad esempio, se un test unitario fallisce, non ci sarà alcun output per i test di integrazione e doc perché quei test verranno eseguiti solo se tutti i test unitari passano.
La prima sezione per i test unitari è la stessa che abbiamo visto: una riga per ogni test unitario (uno denominato internal che abbiamo aggiunto nella Lista 11-12) e quindi una riga di riepilogo per i test unitari.
La sezione dei test di integrazione inizia con la riga Esecuzione dei tests/integration_test.rs. Successivamente, c’è una riga per ogni funzione di test in quel test di integrazione e una riga di riepilogo per i risultati del test di integrazione appena prima che inizi la sezione Doc-tests adder.
Ogni file di test di integrazione ha la propria sezione, quindi se aggiungiamo più file nella directory dei test, 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 determinato file di test di integrazione, utilizzare l’argomento –test di cargo test seguito dal nome del file:
sh
$ cargo test --test integration_test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.64s
Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.0
Sottomoduli nei Test di Integrazione
Aggiungendo più test di integrazione, potresti voler creare più file nella directory dei test per organizzarli meglio; ad esempio, puoi raggruppare le funzioni di test in base alla funzionalità che stanno testando. Come già accennato, ogni file nella directory dei test viene compilato come un proprio crate separato, il che è utile per creare scope separati per imitare più da vicino il modo in cui gli utenti finali utilizzeranno il tuo crate. Tuttavia, questo significa che i file nella directory dei test non condividono lo stesso comportamento dei file in src, come hai imparato nel Capitolo 7 riguardo come separare il codice in moduli e file.
Il comportamento diverso dei file nella directory dei test è più evidente quando hai un insieme di funzioni helper da utilizzare in più file di test di integrazione e cerchi di seguire i passaggi nella sezione “Separazione dei Moduli in File Diversi” del Capitolo 7 per estrarli in un modulo comune. Ad esempio, se creiamo tests/common.rs e inseriamo una funzione chiamata setup al suo interno, possiamo aggiungere del codice a setup che vogliamo chiamare da più funzioni di test in più file di test:
Filename: tests/common.rs
rust
pub fn setup() {
// Qui andrebbe il codice di setup specifico per i test della tua libreria
}
Quando eseguiamo nuovamente i test, vedremo una nuova sezione nell’output dei test per il file common.rs, anche se questo file non contiene alcuna funzione di test e non abbiamo chiamato la funzione setup da nessuna parte:
sh
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.89s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test
test tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/common.rs (target/debug/deps/common-92948b65e88960b4) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4) running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Avere common apparire nei risultati dei test con 0 test eseguiti visualizzato per esso non è ciò che volevamo. Volevamo solo condividere del codice con gli altri file di test di integrazione.
Per evitare che common appaia nell’output dei test, anziché creare tests/common.rs, creeremo tests/common/mod.rs. La directory del progetto ora appare così:
vbnet
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
Questa è la convenzione di denominazione più vecchia che Rust comprende anche, come abbiamo menzionato nella sezione “Percorsi dei File Alternativi” del Capitolo 7. Denominare il file in questo modo dice a Rust di non trattare il modulo common come un file di test di integrazione. Quando spostiamo il codice della funzione di setup in tests/common/mod.rs ed eliminiamo il file tests/common.rs, la sezione nell’output dei test non apparirà più. I file nelle sottodirectory della directory dei test non vengono compilati come crate separati o hanno sezioni nell’output dei test.
Dopo aver creato tests/common/mod.rs, possiamo usarlo da qualsiasi dei file di test di integrazione come un modulo. Ecco un esempio di chiamata della funzione setup dal test it_adds_two in tests/integration_test.rs:
Filename: tests/integration_test.rs
rust
use adder;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}
Nota che la dichiarazione mod common; è la stessa della dichiarazione del modulo che abbiamo dimostrato nella Lista 7-21. Quindi nella funzione di test, possiamo chiamare la funzione common::setup().