Testen in Rust: Unit, Integration, E2E auf fortgeschrittenem Niveau

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

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

Blogs

Buchempfehlungen