Rust è stato progettato fin dall'inizio con un focus su affidabilità e sicurezza, quindi il testing modulare è diventato una parte integrante dell'ecosistema del linguaggio. I test sono integrati a livello di linguaggio e toolchain (cargo test), rendendoli una parte organica del processo di sviluppo.
Storia della questione
Nelle tradizionali ecosistemi (come C/C++, Python, Java) il testing esisteva separato dal programma stesso. In Rust, i test sono parte del codice, vengono compilati e verificati come un modulo completo. Questa sinergia è raggiunta grazie a costrutti linguistici e caratteristiche del compilatore.
Problema
Senza un testing corretto non è possibile garantire l'affidabilità delle funzioni chiave. Per progetti complessi e multimoduli spesso si pone la questione: come organizzare comodamente i test, evitare che dipendano dallo stato di altri moduli e non complicare la struttura del progetto?
Soluzione
In Rust, i test vengono posizionati all'interno dello stesso file sorgente (utilizzando #[cfg(test)]), oppure in una cartella separata tests per il testing di integrazione. A ogni modulo è possibile aggiungere test privati, che hanno accesso all'API privata.
Esempio di codice:
pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add_positive() { assert_eq!(add(2, 3), 5); } #[test] fn test_add_negative() { assert_eq!(add(-1, -3), -4); } }
Caratteristiche chiave:
A cosa serve use super::*; nel modulo di test?
Affinché i test possano accedere a funzioni e strutture del modulo corrente (inclusi quelli privati), nel test si usa di solito use super::*;.
Può #[test] essere asincrono?
Nel linguaggio, per impostazione predefinita #[test] non supporta async, ma con crate esterni (come tokio o async-std) è possibile fare async #[test].
Esempio di codice:
#[tokio::test] async fn test_async_add() { assert_eq!(add(2, 2).await, 4); }
Possono i test cambiare lo stato globale e come evitarlo?
I test in Rust vengono eseguiti parallelamente di default, quindi non è possibile utilizzare uno stato globale condiviso senza sincronizzazione (static mut), altrimenti si verificheranno race condition.
Tutti i test utilizzano una variabile globale:
static mut COUNTER: u32 = 0; #[test] fn test_inc() { unsafe { COUNTER += 1; } }
Pro:
Contro:
Ogni test è isolato, utilizza variabili locali e oggetti mock.
Pro:
Contro: