Testowanie kodu programowego to jeden z najstarszych i najważniejszych procesów w branży programowania. Jednak w wielu językach biblioteki testowe są dostarczane osobno, a integracja testów z głównym kodem może być nieprzezroczysta lub niewygodna. W Rust od pierwszych wersji język był projektowany z myślą o wsparciu wyraźnego testowania modułowego „prosto z pudełka”.
W wielu tradycyjnych językach testy są umieszczane osobno lub wymagają konfiguracji specyficznych frameworków i kompilatorów. Utrudnia to wspólną pracę, zwiększa ryzyko desynchronizacji głównej logiki i testów, a także komplikuje prowadzenie testów integracyjnych i modułowych.
Rust integruje system testowy bezpośrednio na poziomie języka: moduł #[cfg(test)], dyrektywa #[test] oraz narzędzie cargo test pozwalają na tworzenie, uruchamianie i kontrolowanie testów w ramach kodu źródłowego. Zapewnia to ścisłą więź między kodem a testami go weryfikującymi, gwarantuje łatwość uruchamiania testów i automatyzację procesów CI/CD.
Przykład kodu:
// testy automatycznie kompilowane i uruchamiane przez `cargo test` #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two() { assert_eq!(2 + 2, 4); } }
Kluczowe cechy:
cargo test) uruchamia wszystkie znalezione testy.#[cfg(test)] pozwala na nie włączanie testów do wersji produkcyjnej.Czy można używać niestabilnych lub prywatnych funkcji w testach i jak to zrobić?
Tak, można. Wewnątrz modułu testów, ogłoszonego jako #[cfg(test)], testy widzą nie tylko publiczny, ale także prywatny interfejs API bieżącego modułu. Oznacza to, że możesz testować szczegóły implementacji bezpośrednio. Jednak z innego modułu — tylko przez publiczny interfejs.
Przykład:
fn private_internal(x: i32) -> i32 { x + 1 } #[cfg(test)] mod tests { use super::*; #[test] fn test_private() { assert_eq!(private_internal(41), 42); } }
Czy testy o tych samych nazwach będą kolidować pomiędzy różnymi modułami?
Nie, nazwy testów są widoczne tylko w obrębie swojego modułu. Testy z różnych modułów mogą mieć takie same nazwy, ponieważ kompilator odróżnia je po pełnej ścieżce (namespace).
Przykład:
mod a { #[cfg(test)] mod tests { #[test] fn basic() {} } } mod b { #[cfg(test)] mod tests { #[test] fn basic() {} } }
Czy można uruchamiać pojedynczy test lub część zestawu testów?
Tak, dzięki filtrowaniu nazwy testu: cargo test nazwa_testu. To wygodne do debugowania dużych zestawów.
Przykład:
cargo test only_this_test
Programista wyniósł testy do osobnego projektu bez dostępu do prywatnych funkcji i z inną strukturą katalogów. Z powodu zmian w głównej bibliotece testy zaczęły szybko odstawać i tracić aktualność.
Zalety: Można izolować testowanie, unikając przypadkowego umieszczenia modułów testowych w wersji produkcyjnej.
Wady: Utrata synchronizacji, niemożność testowania wewnętrznych szczegółów, niska utrzymywalność i ryzyko nieaktualności testów.
Testy są pisane bezpośrednio w tych samych modułach, co główna logika, małe atomowe przypadki testowe testują prywatne i publiczne API. Nowicjusz szybko zapoznaje się z działaniem modułu poprzez testy.
Zalety: Maksymalna synchronizacja, przejrzystość, przyspieszone testowanie lokalne, przydatność do test-driven development.
Wady: Zwiększenie liczby linii kodu w plikach źródłowych, potencjalny wzrost złożoności przy dużej liczbie testów.