Salta ai contenuti

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.

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 funzione add), così il test può chiamarla.
  • #[test] marca una funzione precisa come test che cargo test deve eseguire.

Dalla cartella del progetto:

Terminal window
cargo test

Vedrai un output come questo:

running 1 test
test tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignored

Il riepilogo è in inglese: 1 passed significa «1 superato», 0 failed «0 falliti», 0 ignored «0 ignorati».

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).

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).

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);
}
}

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 = 5
7 / 2 = 3
▶ Provalo tu

L’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 tests
test tests::rounds_down ... ok
test tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

I 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: 4

E 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).

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.

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).

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 -> A
85 -> B
72 -> C
40 -> F
▶ Provalo tu

cargo 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'.

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)
None
▶ Provalo tu

Confrontala 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.

  • Marca una funzione con #[test] per renderla un test; esegui tutti i test con cargo test.
  • assert!, assert_eq! e assert_ne! coprono quasi tutti i controlli di ogni giorno: preferisci assert_eq!/assert_ne! per avere messaggi di errore migliori.
  • #[should_panic] verifica che una funzione vada in panic quando deve. Aggiungi expected = "..." 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

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