Il collaudo dei programmi può essere un modo molto efficace per mostrare la presenza di difetti, ma è disperatamente inadeguato a mostrare la loro assenza.
Edsger W. Dijkstra, "The Humble Programmer" (1972)
Parliamo di come collaudare il codice Rust. Ciò di cui non parleremo è il modo giusto di collaudare il codice Rust. Ci sono molte scuole di pensiero riguardo il modo giusto o sbagliato di eseguire collaudi. Però, tutti questi approcci usano gli stessi strumenti di base, e quindi mostreremo la sintassi per usarli.
test
Nel caso più semplice, un collaudo in Rust ha un solo test, che è una funzione
annotata con l'attributo test
. Usando Cargo, facciamo un nuovo progetto
chiamato sommatore
:
$ cargo new sommatore
$ cd sommatore
Cargo genera automaticamente un semplice test quando si crea un nuovo progetto.
Ecco il contenuto di src/lib.rs
:
#[test] fn it_works() { }
Si noti il #[test]
. Questo attributo indica che questa è una funzione
di collaudo. Attualmente ha il corpo vuoto. È abbastanza buono per passare!
Possiamo eseguire i test con il comando cargo test
:
$ cargo test
Compiling sommatore v0.0.1 (file:///home/you/projects/sommatore)
Running target/sommatore-91b3e234d4ed382a
running 1 test
test it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests sommatore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Cargo ha compilato ed eseguito il nostro test. Qui ci sono due insiemi di output: uno per il test che abbiamo scritto, e un altro per i test della documentazione. Di quelli ne parleremo dopo. Per adesso, vediamo questa riga:
test it_works ... ok
Si noti il it_works
("funziona"). Deriva dal nome della nostra funzione:
fn it_works() {
Abbiamo anche una riga riassuntiva:
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Quindi perché il nostro test non-fare-niente è passato? Qualunque test
che non va in panic!
passa, e qualunque test che va in panic!
fallisce.
Facciamo fallire il nostro test:
#[test] fn it_works() { assert!(false); }
assert!
è una macro fornita da Rust che prende un argomento: se l'argomento
è true
, non succede niente. Se l'argomento è false
, va in panic!
.
Rieseguiamo i nostri test:
$ cargo test
Compiling sommatore v0.0.1 (file:///home/you/projects/sommatore)
Running target/sommatore-91b3e234d4ed382a
running 1 test
test it_works ... FAILED
failures:
---- it_works stdout ----
thread 'it_works' panicked at 'assertion failed: false', /home/steve/tmp/sommatore/src/lib.rs:3
failures:
it_works
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
thread 'main' panicked at 'Some tests failed', /home/steve/src/rust/src/libtest/lib.rs:247
Rust indica che il nostro test è fallito:
test it_works ... FAILED
E si riflette nella riga riassuntiva:
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
Otteniamo anche un codice di stato non-nullo. Possiamo usare $?
su OS X
o su Linux:
$ echo $?
101
Su Windows, se si usa cmd
:
> echo %ERRORLEVEL%
Mentre se si usa PowerShell:
> echo $LASTEXITCODE # il codice stesso
> echo $? # un booleano, fallimento o successo
Questo è utile se si vuole integrare cargo test
con altre utility.
Possiamo invertire il fallimento del nostro test usando un altro attributo:
should_panic
:
#[test] #[should_panic] fn it_works() { assert!(false); }
Questo test adesso ha successo se va in panic!
e fallisce
termina normalmente. Proviamolo:
$ cargo test
Compiling sommatore v0.0.1 (file:///home/you/projects/sommatore)
Running target/sommatore-91b3e234d4ed382a
running 1 test
test it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests sommatore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Rust fornisce un'altra macro, assert_eq!
, che verifica l'uguaglianza tra
due argomenti:
#[test] #[should_panic] fn it_works() { assert_eq!("Ciao", "mondo"); }
Questo test passerà o fallirà? A causa dell'attribute should_panic
, passerà:
$ cargo test
Compiling sommatore v0.0.1 (file:///home/you/projects/sommatore)
Running target/sommatore-91b3e234d4ed382a
running 1 test
test it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests sommatore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Però, i test should_panic
possono essere fragili, dato che è difficile
garantire che il test non è fallito per una ragione inattesa. Per aiutare
in questa difficoltà, un argomento facoltativo expected
può essere aggiunto
all'attributo should_panic
. L'ambiente di collaudo assicuererà che
il messaggio di fallimento contenga il testo fornito. Una versione più sicura
dell'esempio precedente sarebbe:
#[test] #[should_panic(expected = "asserzione fallita")] fn it_works() { assert_eq!("Hello", "world"); }
E questo è tutto per i fondamenti! Scriviamo un test 'vero':
fn main() {} pub fn somma_due(a: i32) -> i32 { a + 2 } #[test] fn it_works() { assert_eq!(4, somma_due(2)); }pub fn somma_due(a: i32) -> i32 { a + 2 } #[test] fn it_works() { assert_eq!(4, somma_due(2)); }
Questo è un uso molto comune di assert_eq!
: chiamare una funzione
con alcuni argomenti noti, e confrontarne l'esito con l'output atteso.
ignore
Talvolta alcuni test specifici possono richiedere molto tempo per essere
eseguiti. Questi possono essere disabilitati di default usando l'attributo
ignore
:
#[test] fn it_works() { assert_eq!(4, somma_due(2)); } #[test] #[ignore] fn test_costoso() { // codice che impiega un'ora }
Adesso eseguiamo i nostri test e vediamo che it_works
viene eseguito,
ma test_costoso
no:
$ cargo test
Compiling sommatore v0.0.1 (file:///home/you/projects/sommatore)
Running target/sommatore-91b3e234d4ed382a
running 2 tests
test expensive_test ... ignored
test it_works ... ok
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured
Doc-tests sommatore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
I test costosi possono essere eseguiti esplicitamente usando
cargo test -- --ignored
:
$ cargo test -- --ignored
Running target/sommatore-91b3e234d4ed382a
running 1 test
test expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests sommatore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
L'argomento --ignored
è un argomento per il programma di collaudo, e non
per Cargo, e per questo motivo il comando è cargo test -- --ignored
.
tests
C'è un solo aspetto per cui il nostro esempio esistente non è tipico di Rust:
gli manca il modulo tests
. Il modo tipico di scrivere il nostro esempio
è il seguente:
pub fn somma_due(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::somma_due; #[test] fn it_works() { assert_eq!(4, somma_due(2)); } }
Qui ci sono alcuni cambiamenti. Il primo è l'introduzione di un mod tests
con un attributo cfg
. Il modulo ci consente di raggruppare insieme
tutti i nostri test, e anche di definire delle funzioni ausiliare,
se servono, le quali non diventano parte del resto del nostro crate.
L'attributo cfg
compila il nostro codice di collaudo solamente se
attualmente stiamo provando a eseguire i test. Questo può far risparmiare
tempo di compilazione, e inoltre assicura che i nostri test siano
interamente lasciati fuori da una build normale.
Il secondo cambiamento è la dichiarazione use
. Siccome siamo in un modulo
interno, dobbiamo portare la nostra funzione di test nell'ambito. Ciò può
essere seccante in un grande modulo, e perciò questo è un uso tipico
dei globali. Modifichiamo il nostro src/lib.rs
per farne uso:
pub fn somma_due(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { assert_eq!(4, somma_due(2)); } }
Si noti la dversa riga use
. Adesso esuiamo i nostri test:
$ cargo test
Updating registry `https://github.com/rust-lang/crates.io-index`
Compiling sommatore v0.0.1 (file:///home/you/projects/sommatore)
Running target/sommatore-91b3e234d4ed382a
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests sommatore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Funziona!
La convenzione attuale è usare il modulo tests
per tenere i propri test
di unità. Ogni cosa che collauda un pezzettino di funzionalità
ha senso che vada qui. Ma che dire invece dei test di integrazione?
Per quelli c'è la directory tests
.
tests
Ogni file *.rs
nella directory tests
viene trattato come un crate
individuale. Perciò, per scrivere un test di integrazione, creiamo
la directory tests
, e ci mettiamo dentro il file integration_test.rs
,
avente il seguente contenuto:
extern crate sommatore; #[test] fn it_works() { assert_eq!(4, sommatore::somma_due(2)); }
Questo appare simile ai nostri test precedenti, ma è leggermente diverso.
Adesso abbiamo in cima l'istruzione extern crate sommatore
. Questo perché
ogni test della directory tests
è un crate completamente separato, e quindi
dobbiamo importare la nostra libreria.
Questo spiega anche perché tests
è un posto adatto per metterci i test
di integrazione: tali test usano la libreria come lo farebbe qualunque altro
consumatore.
Eseguiamoli:
$ cargo test
Compiling sommatore v0.0.1 (file:///home/you/projects/sommatore)
Running target/sommatore-91b3e234d4ed382a
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Running target/lib-c18e7d3494509e74
running 1 test
test it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests sommatore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Adesso abbiamo tre sezioni: anche il nostro test precedente viene eseguito, così come quello nuovo.
Cargo ignorerà i file nelle sottodirectory della directory tests
.
Perciò in tali sottodirectory si potranno mettere i moduli condivisi dai
test di integrazione.
Per esempio, il file tests/common/mod.rs
non verrà compilato separatamente
da Cargo ma potrà essere importato in ogni test con l'istruzione mod common;
Questo è tutto quel che c'è da dire sulla directory tests
. Qui non serve
il modulo tests
, dato che il tutto si incentra sui test.
Infine andiamo a vedere quella terza sezione: i test della documentazione.
Non c'è niente di meglio che della documentazione con degli esempi.
E non c'è niente di peggio che degli esempi che in realtà non funzionano,
perché il codice è cambiato da quando la documentazione è stata scritta.
A questo scopo, Rust supporta l'esecuzione automatica degli esempi contenuti
nella documentazione del codice (nota: questo funziona solamente
nei crate di libreria, non nei crate di programma). Ecco un src/lib.rs
concretizzato con degli esempi:
//! Il crate `sommatore` fornisce delle funzioni //! che sommano numeri ad altri numeri. //! //! # Esempi //! //! ``` //! assert_eq!(4, sommatore::somma_due(2)); //! ``` /// Questa funzione somma due al suo argomento. /// /// # Esempi /// /// ``` /// use sommatore::somma_due; /// /// assert_eq!(4, somma_due(2)); /// ``` pub fn somma_due(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { assert_eq!(4, somma_due(2)); } }
Si noti che la documentazione a livello di modulo è marcata con //!
, mentre
la documentazione a livello di funzione è marcata con ///
. La documentazione
di Rust supporta il linguaggio Markdown nei commenti, e quindi i blocchi
di codice nei commenti sono marcati da terne di accenti gravi.
Si usa aggiungere la sezione # Esempi
, proprio come sopra, seguita
da uno o più esempi.
Eseguiamo nuovamente i test:
$ cargo test
Compiling sommatore v0.0.1 (file:///home/steve/tmp/sommatore)
Running target/sommatore-91b3e234d4ed382a
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Running target/lib-c18e7d3494509e74
running 1 test
test it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests sommatore
running 2 tests
test somma_due_0 ... ok
test _0 ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
Adesso abbiamo eseguito realmente tutti e tre i tipi di test! Si notino i nomi
dei test di documentazione: il nome _0
viene generato per il test di modulo,
e il nome somma_due_0
per il test di funzione. Questi si autoincrementeranno,
producendo nomi come somma_due_1
quando si aggiungono altri esempi.
Non abbiamo trattato tutti i dettagli della scrittura dei test della documentazione. Per saperne di più, si veda il capitolo della documentazione.