Rust został zaprojektowany z naciskiem na niezawodność i bezpieczeństwo, dlatego testowanie modułowe stało się nieodłączną częścią ekosystemu języka. Testy są zintegrowane na poziomie języka i narzędzi (cargo test), co sprawia, że są organiczną częścią procesu tworzenia oprogramowania.
Historia pytania
W tradycyjnych ekosystemach (np. C/C++, Python, Java) testowanie istniało na uboczu samego programu. W Rust testy są częścią kodu, są kompilowane i sprawdzane jak pełnoprawny moduł. Taki stan rzeczy osiągnięto dzięki konstrukcjom językowym i cechom kompilatora.
Problem
Bez prawidłowego testowania nie można zagwarantować niezawodności kluczowych funkcji. W przypadku skomplikowanych i wielomodulowych projektów często pojawia się pytanie: jak wygodnie organizować testy, aby nie zależały od stanu innych modułów i nie komplikowały struktury projektu?
Rozwiązanie
W Rust testy są umieszczane w samym pliku źródłowym (przy użyciu #[cfg(test)]) lub w osobnym folderze tests do testów integracyjnych. Do każdego modułu można dodać prywatne testy, które mają dostęp do prywatnego API.
Przykład kodu:
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); } }
Kluczowe cechy:
Po co potrzebne jest use super::*; w module testów?
Aby testy mogły korzystać z funkcji i struktur bieżącego modułu (w tym tych prywatnych), w testach zazwyczaj używa się use super::*;.
Czy #[test] może być asynchroniczna?
W języku domyślnie #[test] nie obsługuje async, ale za pomocą zewnętrznych crate (np. tokio lub async-std) można tworzyć asynchroniczne #[test].
Przykład kodu:
#[tokio::test] async fn test_async_add() { assert_eq!(add(2, 2).await, 4); }
Czy testy mogą zmieniać stan globalny i jak tego uniknąć?
Testy w Rust domyślnie są wykonywane równolegle, dlatego nie można używać współdzielonego stanu globalnego bez synchronizacji (static mut), w przeciwnym razie pojawią się wyścigi.
Wszystkie testy używają jednej globalnej zmiennej:
static mut COUNTER: u32 = 0; #[test] fn test_inc() { unsafe { COUNTER += 1; } }
Zalety:
Wady:
Każdy test jest izolowany, używa lokalnych zmiennych i obiektów mockujących.
Zalety:
Wady: