Test in Rust
In Rust i test sono integrati direttamente nel linguaggio e in cargo, quindi per cominciare non ti serve nessuna libreria esterna. Questa pagina spiega quanto basta per scrivere ed eseguire i tuoi primi test.
Scrivere un test di base
Sezione intitolata “Scrivere un test di base”Un test è una normale funzione marcata con #[test]:
fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn it_adds_two_numbers() { assert_eq!(add(2, 3), 5); }}Alcune cose da notare:
#[cfg(test)]dice a Rust che questo modulo viene compilato solo quando esegui i test: non fa parte del tuo programma normale.use super::*;importa tutto dal modulo genitore (qui la funzioneadd), così il test può chiamarla.#[test]marca una funzione precisa come test checargo testdeve eseguire.
Eseguire i test
Sezione intitolata “Eseguire i test”Dalla cartella del progetto:
cargo testVedrai un output come questo:
running 1 testtest tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignoredIl riepilogo è in inglese: 1 passed significa «1 superato», 0 failed «0 falliti», 0 ignored «0 ignorati».
assert! e le sue sorelle
Sezione intitolata “assert! e le sue sorelle”Tre macro coprono quasi tutto quello che ti serve:
#[test]fn checks() { assert!(2 + 2 == 4); // fa fallire il test se la condizione è falsa assert_eq!(2 + 2, 4); // fallisce se i due valori non sono uguali assert_ne!(2 + 2, 5); // fallisce se i due valori SONO uguali}Di solito si preferiscono assert_eq! e assert_ne! a un semplice assert!(a == b): quando falliscono, stampano entrambi i valori, così vedi subito che cosa non corrisponde invece di leggere solo «assertion failed» (asserzione fallita).
Un test che fallisce, e come appare
Sezione intitolata “Un test che fallisce, e come appare”fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn this_will_fail() { assert_eq!(add(2, 2), 5); }}test tests::this_will_fail ... FAILED
assertion `left == right` failed left: 4 right: 5È proprio il tipo di messaggio che vuoi quando un test fallisce: ti dice che cosa ha prodotto davvero il tuo codice (left, il valore a sinistra) rispetto a quello che il test si aspettava (right, il valore a destra).
Verificare che una funzione vada in panic
Sezione intitolata “Verificare che una funzione vada in panic”A volte il comportamento corretto è proprio un panic: per esempio, una funzione che deve rifiutare un input non valido. #[should_panic] serve a verificarlo:
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("cannot divide by zero"); } a / b}
#[cfg(test)]mod tests { use super::*;
#[test] #[should_panic] fn dividing_by_zero_panics() { divide(10, 0); }}Messaggi di errore più chiari
Sezione intitolata “Messaggi di errore più chiari”Quando un test fallisce, vuoi capire il perché il prima possibile. Due piccole aggiunte aiutano molto.
Un messaggio personalizzato. assert!, assert_eq! e assert_ne! accettano tutte un messaggio in più dopo i valori, scritto proprio come un println!. Viene stampato solo se il controllo fallisce.
expected su should_panic. Un semplice #[should_panic] passa con qualsiasi panic, anche uno causato da un bug diverso. Se aggiungi expected = "...", il test passa solo se il messaggio del panic contiene quel testo.
Ecco di nuovo la funzione divide, con un main per poterla eseguire e due test che usano entrambe le idee. Il messaggio del panic resta in inglese perché il test con expected lo confronta proprio con il testo "divide by zero":
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("cannot divide by zero"); } a / b}
fn main() { println!("10 / 2 = {}", divide(10, 2)); println!("7 / 2 = {}", divide(7, 2));}
#[cfg(test)]mod tests { use super::*;
#[test] fn rounds_down() { let result = divide(7, 2); assert_eq!(result, 3, "7 / 2 dovrebbe arrotondare per difetto, invece dà {result}"); }
#[test] #[should_panic(expected = "divide by zero")] fn dividing_by_zero_panics() { divide(10, 0); }}Output:
10 / 2 = 57 / 2 = 3L’output qui sopra è quello che stampa cargo run, perché main non esegue i test. Per eseguire i test usa cargo test. Nel Rust Playground apri il menu accanto a Run e scegli Test. Vedrai:
running 2 teststest tests::rounds_down ... oktest tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered outI test vengono eseguiti in parallelo, quindi le due righe test ... possono comparire in qualsiasi ordine. Ora immagina che il test si aspettasse 4 invece di 3. Il fallimento includerebbe il tuo messaggio, proprio accanto ai due valori:
assertion `left == right` failed: 7 / 2 dovrebbe arrotondare per difetto, invece dà 3 left: 3 right: 4E se divide andasse in panic con un messaggio diverso, il controllo expected se ne accorgerebbe:
note: panic did not contain expected string panic message: "cannot divide by zero" expected substring: "negative"Rust ti dice che il messaggio del panic (panic message) non contiene il testo atteso (expected substring).
Perché vale la pena scrivere test
Sezione intitolata “Perché vale la pena scrivere test”La risposta sincera: per uno script breve, probabilmente non lo farai. I test iniziano a ripagarti quando il codice verrà riusato o modificato più avanti: ti permettono di fare refactoring con tranquillità, perché cargo test ti dice subito se hai rotto qualcosa, invece di scoprirlo da un utente.
I test possono vivere anche nella documentazione: gli esempi di codice dentro i commenti di documentazione /// vengono eseguiti anche loro da cargo test, così i tuoi esempi non possono mai diventare obsoleti. La pagina Documentazione ti mostra come.
Pratica
Sezione intitolata “Pratica”In entrambi gli esercizi la soluzione ha un main che puoi eseguire e dei test da lanciare con cargo test (o con Test nel Playground).
1. Testa una funzione che assegna i voti
Sezione intitolata “1. Testa una funzione che assegna i voti”Scrivi fn grade(score: u32) -> char che restituisce 'A' da 90 in su, 'B' da 80 a 89, 'C' da 70 a 79 e 'F' per qualsiasi punteggio più basso. Poi scrivi dei test che controllano i valori al limite, come 90 e 89. Suggerimento: i bug si nascondono proprio ai limiti. Un test esattamente su 90 scopre l’errore classico di scrivere > invece di >=.
Mostra la soluzione
fn grade(score: u32) -> char { if score >= 90 { 'A' } else if score >= 80 { 'B' } else if score >= 70 { 'C' } else { 'F' }}
fn main() { for score in [95, 85, 72, 40] { println!("{score} -> {}", grade(score)); }}
#[cfg(test)]mod tests { use super::*;
#[test] fn edges_of_a() { assert_eq!(grade(90), 'A'); assert_eq!(grade(89), 'B'); }
#[test] fn edges_of_c() { assert_eq!(grade(70), 'C'); assert_eq!(grade(69), 'F'); }
#[test] fn zero_is_f() { assert_eq!(grade(0), 'F', "un punteggio di 0 deve essere insufficiente"); }}Output:
95 -> A85 -> B72 -> C40 -> Fcargo test dovrebbe riportare 3 passed. Prova a cambiare score >= 90 in score > 90 e riesegui i test: edges_of_a fallisce e ti mostra left: 'B' e right: 'A'.
2. Testa una funzione che restituisce un Option
Sezione intitolata “2. Testa una funzione che restituisce un Option”Scrivi fn safe_divide(a: i32, b: i32) -> Option<i32> che restituisce None quando b vale 0, invece di andare in panic. Scrivi un test per una divisione normale e uno per la divisione per zero. Suggerimento: assert_eq! funziona anche con Option, quindi puoi scrivere assert_eq!(safe_divide(1, 0), None).
Mostra la soluzione
fn safe_divide(a: i32, b: i32) -> Option<i32> { if b == 0 { None } else { Some(a / b) }}
fn main() { println!("{:?}", safe_divide(10, 2)); println!("{:?}", safe_divide(1, 0));}
#[cfg(test)]mod tests { use super::*;
#[test] fn divides_normally() { assert_eq!(safe_divide(10, 2), Some(5)); }
#[test] fn zero_gives_none() { assert_eq!(safe_divide(1, 0), None); }}Output:
Some(5)NoneConfrontala con la funzione divide di prima. Lì la divisione per zero è un panic, quindi il test ha bisogno di #[should_panic]. Qui è solo un normale valore di ritorno, quindi basta un semplice assert_eq!. Le funzioni che restituiscono Option o Result di solito sono più facili da testare.
Riepilogo
Sezione intitolata “Riepilogo”- Marca una funzione con
#[test]per renderla un test; esegui tutti i test concargo test. assert!,assert_eq!eassert_ne!coprono quasi tutti i controlli di ogni giorno: preferisciassert_eq!/assert_ne!per avere messaggi di errore migliori.#[should_panic]verifica che una funzione vada in panic quando deve. Aggiungiexpected = "..."per controllare che sia andata in panic per il motivo giusto.- Aggiungi un messaggio personalizzato a qualsiasi macro di asserzione per spiegare che cosa è andato storto quando fallisce.
- I test valgono di più per il codice che prevedi di modificare o riusare in futuro.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Quale attributo marca una funzione come test da eseguire con cargo test?
2. Perché di solito si preferiscono assert_eq! e assert_ne! a un semplice assert!(a == b)?
3. Che cosa fa #[should_panic]?
Punteggio: 0 / 3
Per approfondire
Sezione intitolata “Per approfondire”- How to Write Tests — The Rust Book (in inglese):
#[test], le macro di asserzione, i messaggi personalizzati,should_panice i test che restituisconoResult. - Controlling How Tests Are Run — The Rust Book (in inglese): eseguire un solo test per nome, mostrare l’output di
println!e#[ignore]. - Test Organization — The Rust Book (in inglese): la differenza tra test unitari e test di integrazione in una cartella
tests/. - Unit testing — Rust by Example (in inglese): brevi esempi di test che passano, falliscono e vanno in panic.
cargo test— The Cargo Book (in inglese): tutte le opzioni del comandocargo test.