Tester une application Runique
Runique teste votre logique métier contre une vraie base de données : chaque test s'exécute dans une transaction toujours annulée, et affiche le SQL qu'il a exécuté à côté de son résultat. C'est le builder runique_test (feature test-utils) et la commande runique test.
1. Mise en place
test-utils se déclare uniquement dans [dev-dependencies] : elle compile du code qui ne doit jamais arriver dans un build de production.
# Cargo.toml
[dependencies]
runique = { version = "3.0.0", features = ["postgres"] }
[dev-dependencies]
runique = { version = "3.0.0", features = ["test-utils"] }
Les tests vivent dans src/runique_test/, un fichier par domaine, déclarés derrière cfg(test) :
// src/main.rs
#[cfg(test)]
mod runique_test;
// src/runique_test/mod.rs
/// Le fichier d'environnement où tous les tests de ce dossier lisent leur base.
pub const ENV: &str = ".env";
mod blog;
mod user;
Le fichier d'environnement doit nommer sa base (DATABASE_URL ou DB_ENGINE) : sinon le test est refusé, plutôt que de créer en silence un fichier SQLite local. Rien n'est lu dans l'environnement du shell, où DATABASE_URL pourrait pointer n'importe où.
2. Écrire un test
Un test est un #[tokio::test] qui renvoie Result<(), TestFailure>, dont la dernière expression est runique_test :
// src/runique_test/blog.rs
use crate::backend::blog::get_article;
use crate::entities::blog;
use runique::prelude::*;
use runique::runique_test::{TestFailure, runique_test};
use sea_orm::DbErr;
#[tokio::test]
async fn un_article_cree_est_trouve_par_son_id() -> Result<(), TestFailure> {
runique_test::<ADb>(super::ENV, async |db| {
let article = blog::ActiveModel {
title: Set("Annulé à la fin du test".to_string()),
..Default::default()
}
.insert(db)
.await?;
get_article(db, article.id)
.await
.map(|_| ())
.ok_or_else(|| DbErr::Custom("l'article doit être trouvé par son id".into()))
})
.await
}
db est un &ADb : le même type que request.engine.db, vos fonctions métier s'exécutent donc telles quelles. Quoi qu'il arrive, la transaction est annulée.
Ce qui fait échouer un test
- le handler renvoie
Err, panique, ou dépasse le délai (RUNIQUE_TEST_TIMEOUTdans le fichier d'environnement, 30 s par défaut) —runique_testne panique jamais, il renvoieErr(TestFailure)avec son message ; - une requête a échoué alors que le handler a renvoyé
Ok: une erreur a été avalée quelque part (.ok(),unwrap_or_default()…) ; - la transaction a été terminée depuis le test (un
COMMITbrut, ou du DDL sous MariaDB/MySQL, qui valide implicitement).
Refus attendus
Un refus que vous voulez (contrainte unique, clé étrangère, NOT NULL) passe par expect_db_error : il s'exécute dans un savepoint — sous Postgres, une instruction en échec hors savepoint gâche toute la transaction — et il est marqué comme attendu dans la trace.
use runique::runique_test::expect_db_error;
let refus = expect_db_error(db, async |sp| {
contribution(id_utilisateur_supprime).insert(sp).await
})
.await?; // Err si la base l'a accepté
3. Lancer les tests
runique test # tous les tests de src/runique_test/
runique test blog # les tests de src/runique_test/blog.rs
runique test blog un_article_cree_est_trouve_par_son_id # un seul test
runique test vérifie d'abord la mise en place — test-utils uniquement en [dev-dependencies] (refusé s'il atteint un build de production via [dependencies], [workspace.dependencies] ou une entrée de [features]), le module runique_test derrière cfg(test), chaque fichier déclaré dans mod.rs — puis lance cargo test un test à la fois, sortie affichée. cargo test seul fonctionne aussi : les tests sont sérialisés dans le processus.
Un autre moteur (MongoDB…) peut se brancher en implémentant le trait TestTransaction ; ADb (SeaORM) est l'implémentation fournie.
4. Contrôles au démarrage
RuniqueApp::builder(config).build().await vérifie la configuration avant de démarrer.
Dans tous les modes :
- Database : ni
.with_database(...)ni.with_database_config(...); - AdminPanel : un préfixe d'admin vide, ou une entrée d'
extra_routesqui nomme une ressource non enregistrée ; - static_cache / media_cache : une valeur de cache qui n'est pas un en-tête HTTP valide ;
- MediaRoot : un dossier media impossible à créer ;
- CORS :
any_origin()combiné àallow_credentials(true).
Hors mode debug uniquement (DEBUG=false), ce qui serait dangereux en production :
- Security : une
SECRET_KEYfaible ; - PublicUrl : le reset de mot de passe ou l'admin activés sans
.with_public_url(...); - ACME :
ACME_ENABLEDsansACME_DOMAINouACME_EMAIL(featureacme).
build() renvoie alors une BuildError — BuildErrorKind::CheckFailed(CheckReport) pour les contrôles ci-dessus, CORS à part — affichée dans le terminal avec une suggestion pour chaque problème.