iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
Rust · Intermédiaire

Rust

Apprendre Rust de zéro jusqu'à un vrai outil en ligne de commande : ownership, emprunts, enums, gestion d'erreurs, traits, génériques, durées de vie, itérateurs, tests, threads et async avec Tokio.

RustCargoédition 2024serdeclapTokio
01Introduction et installation02Variables, types, fonctions et contrôle de flux03Ownership et emprunts04Structs, enums et pattern matching05Collections et gestion d'erreurs06Traits et génériques07Durées de vie et pointeurs intelligents08Closures et itérateurs09Modules, crates et tests10Concurrence : threads, canaux et async11Projet : un gestionnaire de tâches en ligne de commande
Chapitre 9·30 min

Modules, crates et tests

Un programme réel ne tient pas dans main.rs. Rust l'organise en trois niveaux : le package (un projet Cargo, un Cargo.toml), la crate (une unité de compilation, binaire ou bibliothèque) et le module (un espace de noms à l'intérieur d'une crate). Ce chapitre couvre ces trois niveaux, les dépendances, et les tests, intégrés au langage et à Cargo.

Les modules

Un module regroupe des fonctions, des types et d'autres modules. Tout y est privé par défaut ; pub rend un élément visible de l'extérieur.

mod geometrie {
    pub struct Point {
        pub x: f64,
        pub y: f64,
    }
 
    pub fn distance(a: &Point, b: &Point) -> f64 {
        carre(a.x - b.x) + carre(a.y - b.y)
    }
 
    fn carre(v: f64) -> f64 {   // privé : invisible hors du module
        v * v
    }
}
 
fn main() {
    use geometrie::{distance, Point};
    let a = Point { x: 0.0, y: 0.0 };
    let b = Point { x: 3.0, y: 4.0 };
    println!("{}", distance(&a, &b).sqrt()); // 5
}

Rendre une struct pub ne rend pas ses champs publics : chacun a son propre pub. C'est ce qui permet de garantir des invariants. Une struct aux champs privés ne peut être créée que par les fonctions de son module, qui vérifient ce qu'il faut.

Un module par fichier

En pratique, chaque module vit dans son fichier. mod nom; (avec un point-virgule) dit au compilateur d'aller chercher nom.rs ou nom/mod.rs :

src/
├── main.rs
├── config.rs
└── stockage/
    ├── mod.rs        (ou stockage.rs à côté du dossier)
    ├── fichier.rs
    └── memoire.rs
src/main.rs
mod config;
mod stockage;
 
fn main() {
    let c = config::charger();
    stockage::fichier::sauver(&c);
}
src/stockage/mod.rs
pub mod fichier;
pub mod memoire;

Les chemins se lisent comme des dossiers : crate:: part de la racine de la crate, super:: remonte d'un niveau, self:: désigne le module courant. use crée un raccourci local :

use crate::stockage::fichier::sauver;
use std::collections::{HashMap, HashSet};
use std::io::{self, Write};   // importe io et io::Write

Binaire et bibliothèque dans le même projet

Un package peut contenir une crate binaire (src/main.rs) et une crate bibliothèque (src/lib.rs). C'est l'organisation que je recommande dès qu'un projet grossit : toute la logique dans lib.rs, testable et réutilisable, et un main.rs réduit à lire les arguments et appeler la bibliothèque.

src/lib.rs
pub mod calcul;
 
pub fn version() -> &'static str {
    env!("CARGO_PKG_VERSION")
}
src/main.rs
fn main() {
    println!("mon_projet {}", mon_projet::version());
}

Le binaire importe la bibliothèque sous le nom du package, comme n'importe quelle dépendance. La macro env! lit la version de Cargo.toml à la compilation.

Les dépendances

Les bibliothèques Rust s'appellent des crates et se publient sur crates.io. La documentation de chacune est générée automatiquement sur docs.rs.

cargo add serde --features derive
cargo add rand
cargo add --dev pretty_assertions   # seulement pour les tests
Cargo.toml
[dependencies]
rand = "0.9"
serde = { version = "1", features = ["derive"] }
 
[dev-dependencies]
pretty_assertions = "1"

"1" signifie « toute version 1.x compatible », selon le versionnage sémantique. Les versions exactes retenues sont figées dans Cargo.lock : commitez-le pour une application, pour que tout le monde compile la même chose. Les features activent des parties optionnelles d'une crate, ici la génération automatique de code de sérialisation.

Quelques crates qu'on retrouve dans presque tous les projets :

CrateRôle
serde, serde_jsonSérialisation : JSON, TOML, YAML
clapArguments en ligne de commande
anyhow, thiserrorGestion d'erreurs (chapitre 5)
tokioExécution asynchrone (chapitre 10)
reqwestClient HTTP
axumServeur web
sqlxBase de données SQL avec requêtes vérifiées
tracingJournaux structurés
randNombres aléatoires

Avant d'ajouter une dépendance, je regarde trois choses sur crates.io : le nombre de téléchargements récents, la date de la dernière version, et le nombre de dépendances qu'elle tire. Chaque crate ajoutée est du code d'inconnu compilé dans votre programme.

Les tests unitaires

Les tests vivent dans le même fichier que le code, dans un module marqué #[cfg(test)], qui n'est compilé que pour les tests :

src/calcul.rs
pub fn moyenne(valeurs: &[f64]) -> Option<f64> {
    if valeurs.is_empty() {
        return None;
    }
    Some(valeurs.iter().sum::<f64>() / valeurs.len() as f64)
}
 
fn arrondir(v: f64) -> f64 {
    (v * 100.0).round() / 100.0
}
 
#[cfg(test)]
mod tests {
    use super::*;
 
    #[test]
    fn moyenne_simple() {
        assert_eq!(moyenne(&[10.0, 20.0]), Some(15.0));
    }
 
    #[test]
    fn moyenne_vide() {
        assert_eq!(moyenne(&[]), None);
    }
 
    #[test]
    fn arrondi_prive() {
        // les tests unitaires voient aussi les fonctions privées
        assert_eq!(arrondir(3.14159), 3.14);
    }
 
    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn acces_hors_limites() {
        let v: Vec<i32> = Vec::new();
        let _ = v[0];
    }
}
cargo test
running 4 tests
test calcul::tests::acces_hors_limites - should panic ... ok
test calcul::tests::arrondi_prive ... ok
test calcul::tests::moyenne_simple ... ok
test calcul::tests::moyenne_vide ... ok
 
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Les macros à connaître : assert!(condition), assert_eq!(a, b) et assert_ne!(a, b). Elles acceptent un message en plus : assert!(x > 0, "x vaut {x}"). Un test peut aussi renvoyer Result<(), E>, ce qui permet d'utiliser ? dedans.

cargo test moyenne ne lance que les tests dont le nom contient « moyenne ». Les tests tournent en parallèle par défaut : ils ne doivent pas dépendre les uns des autres.

Les tests d'intégration

Le dossier tests/ à la racine contient des tests qui utilisent la bibliothèque comme un utilisateur externe, donc seulement son API publique :

tests/api.rs
use mon_projet::calcul::moyenne;
 
#[test]
fn moyenne_depuis_l_exterieur() {
    assert_eq!(moyenne(&[1.0, 2.0, 3.0]), Some(2.0));
}

Chaque fichier de tests/ est compilé comme une crate séparée. C'est une autre raison de mettre la logique dans lib.rs : un binaire seul ne peut pas être testé ainsi.

Les tests de documentation

Les commentaires /// documentent l'élément qui suit, en Markdown. Les blocs de code qu'ils contiennent sont compilés et exécutés par cargo test :

/// Renvoie la moyenne des valeurs, ou `None` si la tranche est vide.
///
/// ```
/// use mon_projet::calcul::moyenne;
/// assert_eq!(moyenne(&[2.0, 4.0]), Some(3.0));
/// ```
pub fn moyenne(valeurs: &[f64]) -> Option<f64> {
    // ...
}

Une documentation dont les exemples ne compilent plus fait échouer les tests. Elle ne peut donc pas mentir longtemps. cargo doc --open affiche le résultat, avec le même rendu que docs.rs.

L'intégration continue

Trois commandes suffisent à une CI Rust sérieuse :

cargo fmt --check
cargo clippy -- -D warnings
cargo test

-D warnings transforme chaque avertissement de Clippy en erreur. Branchées dans un workflow GitHub Actions, elles bloquent toute pull request mal formatée, maladroite ou cassée.

Exercice

Créez une bibliothèque cargo new --lib texte_utils avec une fonction publique est_palindrome(s: &str) -> bool qui ignore la casse, les espaces et la ponctuation. Écrivez au moins quatre tests, dont un avec des accents, et un exemple dans la documentation.

src/lib.rs
/// Indique si `s` se lit pareil dans les deux sens, en ignorant
/// la casse, les espaces et la ponctuation.
///
/// ```
/// assert!(texte_utils::est_palindrome("Engage le jeu que je le gagne"));
/// ```
pub fn est_palindrome(s: &str) -> bool {
    let lettres: Vec<char> = s
        .chars()
        .filter(|c| c.is_alphanumeric())
        .flat_map(|c| c.to_lowercase())
        .collect();
    lettres.iter().eq(lettres.iter().rev())
}
 
#[cfg(test)]
mod tests {
    use super::*;
 
    #[test]
    fn mot_simple() {
        assert!(est_palindrome("radar"));
    }
 
    #[test]
    fn avec_casse_et_ponctuation() {
        assert!(est_palindrome("Esope reste ici et se repose."));
    }
 
    #[test]
    fn avec_accents() {
        assert!(est_palindrome("Ésé"));
    }
 
    #[test]
    fn pas_un_palindrome() {
        assert!(!est_palindrome("rust"));
    }
}

Le test avec accents passe parce qu'on compare des char, pas des octets. "été" aurait échoué si on avait inversé les octets de la chaîne : le é en occupe deux.


Modules privés par défaut, logique dans lib.rs, dépendances déclarées dans Cargo.toml, tests à côté du code et documentation vérifiée : l'outillage de Rust pousse vers des projets propres sans effort particulier. Le chapitre suivant aborde ce qui fait la réputation du langage : la concurrence sans peur.

Précédent
Closures et itérateurs
Suivant
Concurrence : threads, canaux et async

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main