Fortgeschrittenes Testen in Rust: von Unit bis E2E
Testen ist ein wesentlicher Bestandteil der Entwicklung zuverlässiger Systemsoftware in Rust. Während die grundlegenden Konzepte (einfaches Schreiben von Tests mit #[test]) jedem bekannt sind, erfordern fortgeschrittene Techniken – die Arbeit mit Mocks, Parametrisierung, benutzerdefinierten Attributen und die Organisation von E2E-Tests – eine separate Analyse. In diesem Artikel betrachten wir, wie man das Testen in Rust auf ein professionelles Niveau hebt, indem man Unit-, Integrations- und End-to-End-Ansätze verwendet.
1. Unit-Testen: Mock, Parametrisierung und benutzerdefinierte Attribute
Unit-Tests in Rust werden normalerweise in derselben Datei wie der Code geschrieben, im Modul tests. Auf fortgeschrittenem Niveau besteht jedoch die Notwendigkeit, den zu testenden Code von externen Abhängigkeiten zu isolieren.
Mock-Objekte mit Traits
Zum Erstellen von Mocks in Rust verwendet man am häufigsten Traits. Sie definieren einen Trait, implementieren ihn für das reale Objekt und für den Test-Mock. Beliebte Crates: mockall und mockito (für HTTP).
use mockall::automock;
#[automock]trait Database { fn get_user(&self, id: u32) -> String;}
fn process_user(db: &dyn Database, id: u32) -> String { let user = db.get_user(id); format!("User: {}", user)}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_process_user() { let mut mock = MockDatabase::new(); mock.expect_get_user() .with(predicate::eq(42)) .returning(|_| "Alice".to_string()); assert_eq!(process_user(&mock, 42), "User: Alice"); }}Parametrisierte Tests
Das Wiederholen desselben Tests mit verschiedenen Eingabedaten ist eine häufige Aufgabe. Der Crate rstest bietet eine elegante Lösung.
use rstest::rstest;
#[rstest]#[case(2, 3, 5)]#[case(0, 0, 0)]#[case(-1, 1, 0)]fn test_add(#[case] a: i32, #[case] b: i32, #[case] expected: i32) { assert_eq!(a + b, expected);}Benutzerdefinierte Testattribute
Manchmal ist eine Logik vor oder nach dem Test erforderlich. Man kann eigene Attribute über prozedurale Makros erstellen, aber einfacher ist es, den Crate test-case oder ctor für die Initialisierung zu verwenden.
// Beispiel mit ctor zur Initialisierung eines Loggers#[cfg(test)]mod tests { use ctor::ctor;
#[ctor] fn init() { // Einmalige Initialisierung vor allen Tests }}2. Integrationstesten: Organisation und Arbeit mit externen Abhängigkeiten
Integrationstests in Rust befinden sich im Ordner tests/ auf der Wurzelebene des Projekts. Jede Datei dort ist ein eigener Crate. Fortgeschrittene Techniken umfassen gemeinsame Module und das Testen mit echten Datenbanken oder APIs.
Gemeinsame Hilfsprogramme für Integrationstests
Um Code nicht zu duplizieren (z. B. die Einrichtung eines Testservers), erstellen Sie ein Modul common innerhalb von tests/.
// tests/common/mod.rspub fn setup_test_db() -> String { // Einrichtung einer Testdatenbank "connected".to_string()}
// tests/integration_test.rsmod common;
#[test]fn test_db_connection() { let db = common::setup_test_db(); assert_eq!(db, "connected");}Arbeit mit HTTP-Anfragen
Verwenden Sie zum Testen von HTTP-Clients mockito – es startet einen lokalen Mock-Server.
use mockito::{mock, server_url};
#[tokio::test]async fn test_api_call() { let _m = mock("GET", "/users/1") .with_status(200) .with_body(r#"{"id":1,"name":"Alice"}"#) .create();
let client = reqwest::Client::new(); let resp = client.get(&format!("{}/users/1", server_url())) .send() .await .unwrap(); assert!(resp.status().is_success());}3. E2E-Testen: Vollständiger Anwendungszyklus
End-to-End-Tests überprüfen die Arbeit des gesamten Systems: vom Start