Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Praxisteil & Übungen: Tests und Dokumentation

In diesem Praxisteil widmen wir uns der Softwarequalität und deren Absicherung. Rust bietet von Haus aus erstklassige Werkzeuge, um Code zu testen und verständlich zu dokumentieren. Das Besondere in Rust: Dokumentation und Tests wachsen oft direkt zusammen.

Wir bauen in diesem Kapitel eine Konverter-Bibliothek (converter) für physikalische Einheiten, statten sie mit detaillierter Dokumentation aus und sichern sie mit Unit-Tests, Integrationstests sowie ausführbaren Dokumentations-Tests ab.


1. Didaktische Analogien zur Veranschaulichung

Tests und Dokumentation werden im Programmieralltag oft als lästige Pflicht empfunden. Zwei Analogien zeigen uns, warum diese Sichtweise in Rust überholt ist:

Der Sicherheitsgurt im Fahrzeug (Unit-Tests)

Stellen Sie sich vor, Sie entwickeln ein neues Auto. Wenn Sie den Motor anwerfen, wollen Sie nicht hoffen müssen, dass die Bremsen funktionieren.

  • Unit-Tests (Modultests) sind wie die Sicherheitsgurte und Sensoren an jedem einzelnen Bauteil. Bevor das Auto auf die Straße kommt, wird jede Komponente isoliert im Labor gestresst: Funktioniert der Airbag, wenn der Sensor ein Signal sendet? Hält der Gurt der Belastung stand?
  • In Rust führen wir diese Tests direkt in der gleichen Datei wie den Produktionscode aus. Sie geben uns bei jeder Code-Änderung (Refactoring) die absolute Gewissheit, dass wir bestehende Funktionalität nicht unbemerkt zerstört haben.

Der Beipackzettel mit eingebautem Labortest (Dokumentations-Tests)

Wenn Sie ein Medikament einnehmen, lesen Sie den Beipackzettel, um die richtige Dosierung zu erfahren. Was aber, wenn der Beipackzettel veraltet ist, weil die Zusammensetzung des Medikaments geändert wurde? Das kann gefährlich sein.

  • Dokumentations-Tests (Doc-Tests) in Rust sind wie ein Beipackzettel, den der Compiler bei jedem Labortest (jedem Lauf von cargo test) aktiv ausliest und ausführt.
  • Wir schreiben Anwendungsbeispiele direkt in die Code-Dokumentation. Der Rust-Compiler nimmt diese Code-Beispiele und führt sie wie ein eigenständiges Programm aus. Verändern wir die API und vergessen, die Dokumentation anzupassen, bricht der Testlauf ab. Unsere Dokumentation kann also niemals veralten.

2. Praxis-Szenario: Die physikalische Konverter-Bibliothek

Wir entwickeln ein Software-Modul für Navigationssysteme und Wetterstationen. Das Modul muss:

  1. Temperaturen von Celsius in Fahrenheit umrechnen.
  2. Distanzen von Kilometern in Meilen umrechnen.

Unser Sicherheitsnetz

Wir müssen sicherstellen, dass:

  • Mathematische Rundungsfehler minimiert werden.
  • Ungültige physikalische Werte (z. B. Temperaturen unter dem absoluten Nullpunkt von -273,15 °C) zu einem kontrollierten Programmabbruch (Panic) oder einer Fehlerbehandlung führen.
  • Die Benutzer unserer Bibliothek durch klare Beispiele genau wissen, wie sie die Funktionen aufrufen müssen.

Die Übungsaufgabe befindet sich im Verzeichnis:


3. Der große Test-Katalog: Werkzeuge und Attribute

Hier finden Sie die wichtigsten Werkzeuge für das Schreiben von Tests und Dokumentationen im Detail:

3.1 Unit-Tests (#[test])

Unit-Tests testen kleine, isolierte Einheiten (z. B. eine einzelne Funktion). Sie werden üblicherweise in einem inneren Modul namens tests direkt am Ende der jeweiligen Quellcodedatei platziert:

#![allow(unused)]
fn main() {
#[cfg(test)] // Kompiliert dieses Modul NUR beim Ausführen von 'cargo test'
mod tests {
    use super::*; // Importiert alle Funktionen aus dem übergeordneten Modul

    #[test]
    fn test_beispiel() {
        assert_eq!(2 + 2, 4); // Überprüft Gleichheit
    }
}
}

3.2 Die Test-Zusicherungen (Assertions)

Rust bietet drei Makros, um Testergebnisse zu überprüfen:

  • assert!(bedingung): Prüft, ob die Bedingung true ergibt.
  • assert_eq!(links, rechts): Prüft, ob links und rechts wertgleich sind.
  • assert_ne!(links, rechts): Prüft, ob links und rechts ungleich sind.

3.3 Test-Attribute

  • #[should_panic]: Erwartet, dass der Code in diesem Test abstürzt (eine panic! auslöst). Nur wenn er abstürzt, gilt der Test als bestanden. Ideal, um Fehlertoleranz und Grenzwertverletzungen abzusichern.
  • #[ignore]: Schließt den Test vom normalen Testlauf aus. Nützlich für sehr langsame Tests. Er kann gezielt mit cargo test -- --ignored gestartet werden.

3.4 Dokumentations-Kommentare

  • ///: Dokumentiert das nachfolgende Element (z. B. eine Funktion, Struktur oder ein Modul). Unterstützt Markdown-Formatierung. Codeblöcke darin werden automatisch als Doc-Tests ausgeführt.
  • //!: Dokumentiert das übergeordnete Element (z. B. die gesamte Library-Datei ganz oben).

4. Aufgabenstellung

  1. Erstellen Sie ein neues Library-Projekt mit cargo new --lib converter.
  2. Implementieren Sie eine öffentliche Funktion celsius_to_fahrenheit(c: f64) -> f64.
    • Formel: $F = C \cdot 1,8 + 32$
    • Sicherheitsregel: Wenn die Temperatur unter dem absoluten Nullpunkt ($-273,15$ °C) liegt, soll die Funktion mit einer verständlichen Fehlermeldung abstürzen (panic!).
  3. Schreiben Sie für diese Funktion einen Dokumentations-Kommentar (///) inklusive eines Beispiels im Markdown-Format, das die Anwendung und das erwartete Ergebnis zeigt.
  4. Implementieren Sie eine zweite öffentliche Funktion km_to_miles(km: f64) -> f64.
    • Formel: $M = km \cdot 0,621371$
    • Sicherheitsregel: Negative Kilometerwerte sind physikalisch unsinnig; lösen Sie in diesem Fall eine panic! aus.
  5. Erstellen Sie ein inneres Testmodul tests mit folgenden Unit-Tests:
    • test_celsius_to_fahrenheit_normal: Überprüft Standardwerte (z. B. $0$ °C $\rightarrow 32$ °F, $100$ °C $\rightarrow 212$ °F).
    • test_celsius_unter_absolutem_nullpunkt: Sichert mit #[should_panic] ab, dass Werte unter $-273,15$ °C zum Absturz führen.
    • test_km_to_miles_normal: Überprüft die korrekte Umrechnung.
    • test_km_negativ_panic: Sichert ab, dass negative Distanzen abstürzen.
  6. Erstellen Sie einen Integrationstest in einer separaten Datei unter tests/integration_tests.rs. Dieser soll die gesamte API von außen testen (wie ein externer Nutzer) und beide Funktionen in einem Ablauf kombinieren.

5. Detaillierte Code-Erklärung der Musterlösung

Der Bibliotheks-Code (src/lib.rs)

#![allow(unused)]
fn main() {
//! Eine einfache und performante Konverter-Bibliothek für
//! physikalische Einheiten (Temperatur und Distanz).

/// Der absolute Nullpunkt in Grad Celsius.
pub const ABSOLUTE_ZERO_CELSIUS: f64 = -273.15;

/// Konvertiert Grad Celsius in Grad Fahrenheit.
///
/// # Formel
/// `Fahrenheit = Celsius * 1.8 + 32`
///
/// # Panics
/// Die Funktion stürzt ab, wenn die übergebene Temperatur unter dem absoluten Nullpunkt
/// (-273,15 °C) liegt.
///
/// # Beispiele
/// ```
/// use converter::celsius_to_fahrenheit;
/// 
/// let gefrierpunkt = celsius_to_fahrenheit(0.0);
/// assert_eq!(gefrierpunkt, 32.0);
/// 
/// let siedepunkt = celsius_to_fahrenheit(100.0);
/// assert_eq!(siedepunkt, 212.0);
/// ```
pub fn celsius_to_fahrenheit(celsius: f64) -> f64 {
    if celsius < ABSOLUTE_ZERO_CELSIUS {
        panic!("Temperatur ({:.2}°C) liegt unter dem absoluten Nullpunkt!", celsius);
    }
    celsius * 1.8 + 32.0
}

/// Konvertiert Kilometer in Meilen.
///
/// # Panics
/// Die Funktion stürzt ab, wenn ein negativer Distanzwert übergeben wird.
///
/// # Beispiele
/// ```
/// use converter::km_to_miles;
/// 
/// let distanz = km_to_miles(10.0);
/// // Vergleich mit Toleranz aufgrund von Fließkomma-Ungenauigkeiten
/// assert!((distanz - 6.21371).abs() < 1e-5);
/// ```
pub fn km_to_miles(km: f64) -> f64 {
    if km < 0.0 {
        panic!("Distanz ({:.2} km) darf nicht negativ sein!", km);
    }
    km * 0.621371
}

// ---------------------------------------------------------
// Modultests (Unit-Tests)
// ---------------------------------------------------------
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_celsius_to_fahrenheit_normal() {
        assert_eq!(celsius_to_fahrenheit(0.0), 32.0);
        assert_eq!(celsius_to_fahrenheit(100.0), 212.0);
        assert_eq!(celsius_to_fahrenheit(-40.0), -40.0); // Schnittpunkt der Skalen
    }

    #[test]
    #[should_panic(expected = "liegt unter dem absoluten Nullpunkt!")]
    fn test_celsius_unter_absolutem_nullpunkt() {
        celsius_to_fahrenheit(-280.0);
    }

    #[test]
    fn test_km_to_miles_normal() {
        let ergebnis = km_to_miles(1.0);
        assert!((ergebnis - 0.621371).abs() < 1e-6);
    }

    #[test]
    #[should_panic(expected = "darf nicht negativ sein!")]
    fn test_km_negativ_panic() {
        km_to_miles(-5.0);
    }
}
}

Der Integrationstest (tests/integration_tests.rs)

#![allow(unused)]
fn main() {
// Integrationstests liegen außerhalb des src/-Verzeichnisses.
// Sie binden die Bibliothek wie ein externer Nutzer ein.
use converter::{celsius_to_fahrenheit, km_to_miles};

#[test]
fn test_kombinierte_konvertierung() {
    // Ein Anwender läuft 5 Kilometer bei 20 Grad Celsius
    let distanz_meilen = km_to_miles(5.0);
    let temp_fahrenheit = celsius_to_fahrenheit(20.0);
    
    assert!(distanz_meilen > 3.0 && distanz_meilen < 3.2);
    assert_eq!(temp_fahrenheit, 68.0);
}
}

Anatomische Zeilenzerlegung der Lösung

  • Zeile 1: //! ... – Ein Modul-Dokumentations-Kommentar. Dieser dokumentiert den gesamten Crate-Inhalt und wird auf der Startseite der automatisch generierten Dokumentation (via cargo doc) angezeigt.
  • Zeile 13: /// # Panics – Markdown-Überschriften in Doc-Kommentaren sind standardisiert. Abschnitte wie # Panics, # Errors oder # Examples helfen dem Anwender, kritische API-Details auf einen Blick zu erfassen.
  • Zeile 17: use converter::celsius_to_fahrenheit; – Im Doc-Test müssen wir unsere Bibliothek explizit einbinden (use), da der Doc-Test als eigenständiges kleines Programm übersetzt wird und nicht im Scope des Moduls ausgeführt wird.
  • Zeile 49: #[cfg(test)] – Dieses Attribut teilt dem Compiler mit, dass das gesamte Modul tests nur dann übersetzt werden soll, wenn wir cargo test ausführen. Beim normalen Kompilieren für die Produktion (cargo build oder cargo run) wird der gesamte Test-Code komplett ignoriert, was zu kleineren Binärdateien führt.
  • Zeile 57: assert_eq!(celsius_to_fahrenheit(-40.0), -40.0); – Testet den Schnittpunkt, an dem Celsius- und Fahrenheit-Werte mathematisch identisch sind.
  • Zeile 61: #[should_panic(expected = "...")] – Dieser Test verifiziert, dass die Grenzprüfung korrekt anschlägt. Das Argument expected erlaubt es uns, den exakten Text der Fehlermeldung zu prüfen, damit der Test nicht fälschlicherweise durch einen anderen unerwarteten Absturz grün wird.
  • Zeile 68: assert!((ergebnis - 0.621371).abs() < 1e-6);Sehr wichtig bei Gleitkommazahlen! Aufgrund von binären Rundungsfehlern sollten Sie Fließkommazahlen niemals direkt auf Gleichheit (== bzw. assert_eq!) prüfen. Stattdessen zieht man die Werte voneinander ab, nimmt den absoluten Betrag (.abs()) und prüft, ob dieser kleiner als eine sehr kleine Toleranzschwelle (Epsilon, hier 1e-6) ist.

6. Typische Compilerfehler & Fehlerbehebung (CDD-Ansatz)

Wir besprechen typische Stolpersteine beim Testen und Dokumentieren in Rust.

Fehler 1: Private APIs in Doc-Tests nutzen

#![allow(unused)]
fn main() {
/// ```
/// use converter::geheime_hilfsfunktion; // COMPILER-FEHLER!
/// ```
fn geheime_hilfsfunktion() {}
}
  • Ursache: Da Doc-Tests wie eine externe Crate getestet werden, können sie nur auf öffentliche APIs (pub) zugreifen. Private Hilfsfunktionen können auf diese Weise nicht dokumentiert und getestet werden.
  • Lösung: Testen Sie private Funktionen ausschließlich über Unit-Tests innerhalb des inneren Moduls tests am Ende der Datei. Unit-Tests haben vollen Zugriff auf alle privaten Elemente, da sie sich innerhalb desselben Moduls befinden.

Fehler 2: Fehlendes use super::*; im Test-Modul

#![allow(unused)]
fn main() {
mod tests {
    #[test]
    fn test_aufruf() {
        let x = celsius_to_fahrenheit(10.0); // COMPILER-FEHLER: cannot find function!
    }
}
}
  • Ursache: In Rust definieren Module eigenständige Namensräume. Das innere Modul tests sieht die Funktionen des übergeordneten Moduls nicht automatisch.
  • Lösung: Fügen Sie am Anfang des tests-Moduls immer use super::*; ein. Dies importiert alle Elemente des äußeren Moduls in den Scope des Testmoduls.

Fehler 3: Doc-Tests schlagen fehl, weil Crate-Name unbekannt ist

error[E0433]: failed to resolve: use of undeclared crate or module `converter`
 --> src/lib.rs:17:5
  |
5 | use converter::celsius_to_fahrenheit;
  |     ^^^^^^^^^ use of undeclared crate or module
  • Ursache: Der in use ... angegebene Name im Doc-Test entspricht nicht dem Crate-Namen, der in der Datei Cargo.toml definiert ist.
  • Lösung: Stellen Sie sicher, dass Sie im Doc-Test genau den Bibliotheksnamen verwenden, der unter [package] name = "..." in Ihrer Cargo.toml hinterlegt ist.