Rust en pratique#
Ce dernier chapitre rassemble les outils, conventions et pratiques qui font passer un programme Rust du prototype au projet maintenable. Nous y retrouverons de nombreux concepts des chapitres précédents — types (chapitre 3), traits (chapitre 13), erreurs (chapitre 12), modules (chapitre 11) — dans un flux de développement professionnel.
Tests#
Rust intègre un cadre de tests directement dans le langage et dans Cargo. Aucune bibliothèque externe n’est nécessaire pour écrire et exécuter des tests.
Tests unitaires#
Définition 145 (Fonction de test)
Une fonction de test est une fonction annotée par #[test]. Elle ne prend aucun argument et ne retourne rien (ou un Result<(), E>). Cargo l’exécute via cargo test. Si la fonction panique, le test échoue ; sinon, il réussit.
Exemple 123 (Module de tests unitaires)
Par convention, les tests unitaires sont placés dans un module tests encadré par #[cfg(test)], qui limite sa compilation au contexte de test.
pub fn factorielle(n: u64) -> u64 {
(1..=n).product()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_factorielle_cinq() {
assert_eq!(factorielle(5), 120);
}
#[test]
#[should_panic(expected = "overflow")]
fn test_depassement() {
let _ = factorielle(100);
}
}
Les macros d’assertion sont l’outil principal pour vérifier les résultats.
assert!(2 + 2 == 4);
assert_eq!(vec![1, 2, 3].len(), 3);
assert_ne!("rust", "java");
println!("Toutes les assertions passent.");
Toutes les assertions passent.
Remarque 118
Les macros d’assertion acceptent un message formaté en argument optionnel : assert_eq!(a, b, "attendu {} mais obtenu {}", b, a). Ce message apparaît en cas d’échec et facilite le diagnostic.
Tests d’intégration#
Définition 146 (Test d’intégration)
Les tests d’intégration sont des fichiers placés dans le répertoire tests/ à la racine du projet. Chaque fichier constitue une crate séparée qui importe la bibliothèque publique. Ils testent l’interface externe, comme le ferait un utilisateur du crate.
Exemple 124 (Structure d’un test d’intégration)
// fichier : tests/integration.rs
use mon_projet::factorielle;
#[test]
fn test_depuis_lexterieur() {
assert_eq!(factorielle(6), 720);
}
On exécute l’ensemble des tests avec cargo test, ou un fichier spécifique avec cargo test --test integration.
Proposition 43 (Isolation des tests)
Cargo exécute chaque test dans un thread séparé et capture la sortie standard. L’option -- --nocapture affiche les println! pendant l’exécution. L’option -- --test-threads=1 force l’exécution séquentielle lorsque les tests partagent un état global.
Documentation#
Rust traite la documentation comme un citoyen de première classe : les commentaires de documentation sont compilés en HTML par cargo doc et les exemples qu’ils contiennent sont exécutés comme des tests.
Définition 147 (Commentaires de documentation)
Rust distingue deux formes de commentaires de documentation :
///documente l’élément qui suit (fonction, structure, etc.) ;//!documente l’élément qui contient le commentaire (module, crate).
Les deux supportent la syntaxe Markdown complète.
Exemple 125 (Documentation d’une fonction publique)
/// Calcule la distance euclidienne entre deux points.
///
/// # Exemples
///
/// \`\`\`
/// let d = mon_projet::distance(0.0, 0.0, 3.0, 4.0);
/// assert_eq!(d, 5.0);
/// \`\`\`
pub fn distance(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
}
Les sections conventionnelles sont # Arguments, # Exemples, # Panics, # Errors et # Safety. Le contenu de # Exemples est exécuté par cargo test en tant que doctest.
Remarque 119
Les doctests garantissent que les exemples restent synchronisés avec le code. Si un exemple ne compile plus, cargo test le signale. La commande cargo doc --open génère la documentation HTML du projet. Le site docs.rs héberge automatiquement la documentation de tous les crates publiés.
Attributs#
Définition 148 (Attribut)
Un attribut est une annotation de la forme #[...] (appliqué à l’élément suivant) ou #![...] (appliqué à l’élément englobant). Les attributs guident le compilateur, activent des dérivations (chapitre 8), contrôlent la compilation conditionnelle et configurent les avertissements. Nous en avons déjà rencontré plusieurs : #[derive], #[test], #[cfg(test)].
#[derive]#
#[derive] génère l’implémentation de certains traits (cf. chapitre 13). Les traits dérivables courants sont Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd et Ord.
#[derive(Debug, Clone, PartialEq, Default)]
struct Config {
largeur: u32,
hauteur: u32,
titre: String,
}
let c1 = Config { largeur: 800, hauteur: 600, titre: String::from("Fenêtre") };
let c2 = c1.clone();
assert_eq!(c1, c2);
println!("{:?}", Config::default());
Config { largeur: 0, hauteur: 0, titre: "" }
#[cfg] — compilation conditionnelle#
Exemple 126 (Compilation conditionnelle)
L’attribut #[cfg] inclut ou exclut du code selon la cible de compilation ou des drapeaux personnalisés.
#[cfg(target_os = "linux")]
fn plateforme() -> &'static str { "Linux" }
#[cfg(target_os = "windows")]
fn plateforme() -> &'static str { "Windows" }
Autres attributs courants#
Proposition 44 (Attributs fréquemment utilisés)
Attribut |
Effet |
|---|---|
|
Supprime les avertissements sur les éléments inutilisés |
|
Oblige l’appelant à utiliser la valeur retournée |
|
Suggère au compilateur d’inliner la fonction |
|
Impose une disposition mémoire compatible C (cf. chapitre 22) |
|
Interdit tout bloc |
|
Empêche le filtrage exhaustif depuis un crate externe |
#[must_use]
fn calculer_important() -> i32 { 42 }
let resultat = calculer_important();
println!("Résultat : {}", resultat);
Résultat : 42
Clippy et rustfmt#
Le chapitre 1 a présenté Clippy et rustfmt. Revenons-y maintenant que nous maîtrisons le langage.
rustfmt#
rustfmt formate le code selon les conventions officielles, configurable via rustfmt.toml.
Exemple 127 (Configuration de rustfmt)
// rustfmt.toml
max_width = 100
tab_spaces = 4
imports_granularity = "Crate"
cargo fmt reformate le projet. cargo fmt -- --check vérifie sans modifier — utile en intégration continue.
Clippy#
Clippy dispose de plus de 700 lints par catégories : correctness, style, complexity, perf, pedantic.
Exemple 128 (Lints Clippy utiles)
Clippy suggère is_empty() au lieu de len() == 0, détecte les clones inutiles, les conversions redondantes et les boucles remplaçables par des itérateurs (cf. chapitre 16). On active le groupe pedantic pour un audit plus strict : cargo clippy -- -W clippy::pedantic.
Remarque 120
Il est idiomatique d’exécuter cargo clippy -- -D warnings en CI : l’option -D warnings transforme les avertissements en erreurs. Combiné à cargo fmt -- --check, cela garantit un niveau de qualité minimal sur chaque pull request.
L’écosystème#
L’un des atouts majeurs de Rust est son écosystème de bibliothèques, publiées sur crates.io.
Définition 149 (Crates fondamentaux de l’écosystème)
Crate |
Domaine |
Description |
|---|---|---|
serde |
Sérialisation |
Sérialisation/désérialisation générique via |
tokio |
Asynchrone |
Runtime asynchrone (cf. chapitre 20), I/O non bloquant |
clap |
CLI |
Analyse d’arguments en ligne de commande avec dérivation |
anyhow |
Erreurs (applis) |
Type d’erreur effacé pour les applications, contexte enrichi |
thiserror |
Erreurs (libs) |
Dérivation de types d’erreur personnalisés (cf. chapitre 12) |
tracing |
Observabilité |
Journalisation structurée et traçage distribué |
rayon |
Parallélisme |
Parallélisme de données par itérateurs (cf. chapitre 16) |
reqwest |
HTTP |
Client HTTP asynchrone, basé sur tokio |
Exemple 129 (Serde — sérialisation dérivée)
La combinaison de serde et serde_json illustre la puissance de la dérivation (chapitre 13). Un simple #[derive(Serialize, Deserialize)] suffit à rendre une structure sérialisable en JSON, TOML, YAML, etc.
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct Utilisateur { nom: String, age: u32, actif: bool }
fn main() -> Result<(), serde_json::Error> {
let u = Utilisateur { nom: String::from("Alice"), age: 30, actif: true };
let json = serde_json::to_string_pretty(&u)?;
let u2: Utilisateur = serde_json::from_str(&json)?;
println!("{json}\n{:?}", u2);
Ok(())
}
Remarque 121
Pour découvrir et évaluer des crates : lib.rs offre une navigation par catégorie, docs.rs héberge la documentation, et blessed.rs recense les crates recommandés par la communauté.
Profiling et benchmarks#
Mesurer les performances est indispensable. Rappelons (chapitre 1) : toujours compiler en --release avant toute mesure.
Définition 150 (Benchmarks en Rust)
cargo bench compile et exécute les fonctions annotées #[bench], mais ce cadre n’est disponible que sur le canal nightly. En pratique, la communauté utilise Criterion, qui fonctionne sur stable et produit des analyses statistiques rigoureuses.
Exemple 130 (Benchmark avec Criterion)
// benches/mon_bench.rs
use criterion::{criterion_group, criterion_main, Criterion};
use mon_projet::factorielle;
fn bench_fact(c: &mut Criterion) {
c.bench_function("factorielle(20)", |b| b.iter(|| factorielle(20)));
}
criterion_group!(benches, bench_fact);
criterion_main!(benches);
Criterion mesure le temps d’exécution, calcule des intervalles de confiance et détecte les régressions entre exécutions.
Remarque 122
Pour le profiling fin : flamegraph (cargo install flamegraph) génère des flame graphs via perf (Linux) ou dtrace (macOS). Le profiling doit être réalisé en mode --release avec debug = true dans [profile.release].
Bonnes pratiques#
Cette section distille les idiomes qui caractérisent un code Rust de qualité.
Privilégier les types expressifs#
Proposition 45 (Encoder les invariants dans les types)
Le système de types (chapitres 3, 8, 9) est suffisamment riche pour encoder de nombreux invariants à la compilation. Préférer un enum à un booléen, un newtype à un type primitif. Chaque information encodée dans le type est une erreur en moins à l’exécution.
// Mauvais : fn envoyer(message: &str, urgent: bool)
// Bon : l'enum est explicite et extensible
#[derive(Debug)]
enum Priorite { Normale, Urgente, Critique }
fn envoyer(message: &str, priorite: Priorite) {
println!("[{:?}] {}", priorite, message);
}
envoyer("Serveur redémarré", Priorite::Urgente);
[Urgente] Serveur redémarré
Utiliser les itérateurs#
Les itérateurs (chapitre 16) et les fermetures (chapitre 17) sont au coeur du Rust idiomatique.
let donnees = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let somme_pairs: i32 = donnees.iter()
.filter(|&&x| x % 2 == 0)
.sum();
assert_eq!(somme_pairs, 30);
println!("Somme des pairs : {}", somme_pairs);
Somme des pairs : 30
Gérer les erreurs avec soin#
Remarque 123
Le chapitre 12 a détaillé la gestion des erreurs. En pratique :
Bibliothèques : définir un type d’erreur propre (avec
thiserror) et retournerResult. Ne jamais paniquer au nom de l’appelant.Applications : utiliser
anyhow::Resultpour propager les erreurs avec contexte.Prototypes :
unwrap()est acceptable pendant l’exploration, mais doit être remplacé avant la mise en production.
Patterns courants#
Proposition 46 (Idiomes Rust récurrents)
Pattern |
Description |
|---|---|
Newtype |
Encapsuler un type pour la sécurité de typage (chapitre 8) |
Builder |
Construire un objet complexe en chaînant des méthodes retournant |
Type state |
Encoder les transitions d’état dans les types, rendant les états invalides impossibles |
RAII |
Lier la durée de vie d’une ressource à celle d’une valeur (chapitre 5) |
|
Fournir des conversions idiomatiques entre types (chapitre 13) |
Itérateur personnalisé |
Implémenter |
Exemple 131 (Le pattern Builder)
Le Builder construit un objet étape par étape via des méthodes qui consomment et retournent self. Il est omniprésent dans l’écosystème (std::process::Command, reqwest::Client, clap::Command).
struct RequeteBuilder { url: String, methode: String, timeout_ms: u64 }
impl RequeteBuilder {
fn new(url: &str) -> Self {
Self { url: url.to_string(), methode: String::from("GET"), timeout_ms: 5000 }
}
fn methode(mut self, m: &str) -> Self { self.methode = m.to_string(); self }
fn timeout(mut self, ms: u64) -> Self { self.timeout_ms = ms; self }
}
let req = RequeteBuilder::new("https://api.exemple.fr")
.methode("POST")
.timeout(3000);
Conseils pour la suite#
Remarque 124
Pour aller plus loin après ce livre :
Lire du code existant : les crates populaires (ripgrep, serde, tokio) sont d’excellentes sources d’apprentissage.
Contribuer : corriger un bug ou améliorer la documentation d’un crate est le meilleur exercice.
Pratiquer : Exercism, Rustlings et Advent of Code offrent des exercices progressifs.
Explorer les RFC : le processus RFC de Rust est ouvert, permettant de comprendre les décisions de conception.
Ce chapitre clôt notre parcours. Des premiers pas avec Cargo (chapitre 1) jusqu’au code unsafe (chapitre 22), nous avons exploré un langage alliant sécurité mémoire, performance et expressivité. Les outils présentés ici — tests, documentation, linting, benchmarks — transforment la connaissance du langage en projets fiables. La suite appartient à la pratique.