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:
- Temperaturen von Celsius in Fahrenheit umrechnen.
- 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 Bedingungtrueergibt.assert_eq!(links, rechts): Prüft, oblinksundrechtswertgleich sind.assert_ne!(links, rechts): Prüft, oblinksundrechtsungleich sind.
3.3 Test-Attribute
#[should_panic]: Erwartet, dass der Code in diesem Test abstürzt (einepanic!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 mitcargo test -- --ignoredgestartet 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
- Erstellen Sie ein neues Library-Projekt mit
cargo new --lib converter. - 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!).
- Schreiben Sie für diese Funktion einen Dokumentations-Kommentar (
///) inklusive eines Beispiels im Markdown-Format, das die Anwendung und das erwartete Ergebnis zeigt. - 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.
- Erstellen Sie ein inneres Testmodul
testsmit 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.
- 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 (viacargo doc) angezeigt. - Zeile 13:
/// # Panics– Markdown-Überschriften in Doc-Kommentaren sind standardisiert. Abschnitte wie# Panics,# Errorsoder# Exampleshelfen 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 Modultestsnur dann übersetzt werden soll, wenn wircargo testausführen. Beim normalen Kompilieren für die Produktion (cargo buildodercargo 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 Argumentexpectederlaubt 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, hier1e-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
testsam 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
testssieht die Funktionen des übergeordneten Moduls nicht automatisch. - Lösung: Fügen Sie am Anfang des
tests-Moduls immeruse 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 DateiCargo.tomldefiniert ist. - Lösung: Stellen Sie sicher, dass Sie im Doc-Test genau den Bibliotheksnamen verwenden, der unter
[package] name = "..."in IhrerCargo.tomlhinterlegt ist.