Tests
Des tests rapides et fiables doivent piloter votre application comme le ferait un client — sans
démarrer de serveur ni toucher au réseau. Le TestClient de Rustango exécute votre
routeur in-process : vous appelez client.get("/path"), il achemine la requête
à travers la vraie pile (extractors, middleware, handlers) et vous renvoie la
réponse sur laquelle faire des assertions. Ajoutez l'isolation par rollback de transaction pour les tests de base de données et
un ensemble d'assertions de réponse, et vous obtenez un environnement de tests
d'intégration complet sans quitter le processus.
Un terme nouveau ici ? router, handler, fixture, rollback — voir le glossaire.
Source :
rustango::test_client(TestClient,TestResponse),rustango::test_assertions(assert_status_2xx,assert_redirects,assert_cookie_set, …), etrustango::test_db(with_rollback) — toujours compilés.Version exécutable : les snippets ci-dessous sont un test qui passe —
testing_doc.rs(cargo test -p rustango --test testing_doc). Presque tous les autres*_doc.rsde ce dépôt utilisentTestClientde la même manière.
Table des matières
- Étape 1 — Pilotez votre application avec TestClient
- Étape 2 — Faites des assertions sur la réponse
- Envoyer du JSON, des en-têtes et des corps
- Tester une vraie API
- Tests de base de données avec rollback
- Suites live, et pourquoi une exécution verte peut ne rien prouver
- Helpers d'assertion de réponse
- Voir aussi
Étape 1 — Pilotez votre application avec TestClient
Enveloppez n'importe quel axum::Router dans un TestClient et envoyez des requêtes — aucun socket n'est lié,
aucune tâche serveur n'est lancée. La requête traverse votre vrai middleware et vos
handlers :
use rustango::test_client::TestClient;
let client = TestClient::new(app()); // app() returns your Router
let res = client.get("/ping").send().await; // routed in-process
assert_eq!(res.status, 200);
TestClient a get / post / put / patch / delete / head, chacun
renvoyant un builder que vous finissez avec .send().await.
Étape 2 — Faites des assertions sur la réponse
TestResponse expose le statut et le corps dans la forme dont vous avez besoin :
let res = client.get("/ping").send().await;
res.status; // u16 — e.g. 200
res.text(); // body as a String
res.header("content-type"); // Option<&str>
// JSON, two ways:
let res = client.post("/echo").json(&json!({ "name": "Ada" })).send().await;
assert_eq!(res.json_value()["name"], "Ada"); // untyped
#[derive(serde::Deserialize)]
struct Out { name: String }
let out: Out = res.json(); // typed
assert_eq!(out.name, "Ada");
Envoyer du JSON, des en-têtes et des corps
Le builder de requête enchaîne tout avant .send() :
let res = client
.post("/api/posts")
.header("authorization", "Bearer <token>") // auth, content negotiation, …
.json(&json!({ "title": "Hello", "body": "..." }))
.send()
.await;
assert_eq!(res.status, 201);
Utilisez .body(...) pour les corps bruts (non-JSON), et une route manquante renvoie un vrai
404 — vérifié dans le test sous-jacent.
Tester une vraie API
app() dans vos tests n'est que votre routeur. Pour une API adossée à une DB, construisez-la exactement
comme le fait main.rs mais avec un pool de test — le motif que la plupart des tests *_doc.rs utilisent :
async fn app() -> axum::Router {
let pool = test_pool().await; // a sqlite::memory: or test DB pool
PostViewSet::router("/api/posts", pool)
}
#[tokio::test]
async fn create_then_list() {
let client = TestClient::new(app().await);
let created = client.post("/api/posts")
.json(&json!({ "title": "Hi", "body": "b" }))
.send().await;
assert_eq!(created.status, 201);
let list = client.get("/api/posts").send().await;
assert!(list.json_value()["results"].is_array());
}
C'est le test ViewSets de ce guide — le même TestClient.
Tests de base de données avec rollback
Les tests qui écrivent dans une base de données ne doivent pas laisser fuir leur état les uns dans les autres.
test_db::with_rollback exécute votre test à l'intérieur d'une transaction et la rollback
à la fin, de sorte que chaque test démarre depuis le même état propre et que rien ne persiste :
use rustango::test_db::with_rollback;
#[tokio::test]
async fn creating_a_post_persists_it() {
with_rollback(&pool, |tx| async move {
// ... insert + assert against `tx` ...
// everything here is rolled back when the closure returns
}).await;
}
Pour SQLite, les tests *_sqlite_live.rs à travers ce dépôt utilisent à la place une base de données
en mémoire par test — également entièrement isolée, avec zéro configuration externe.
Suites live, et pourquoi une exécution verte peut ne rien prouver
Les tests nommés *_live.rs parlent à une vraie base de données. La plupart
n'ont besoin de rien de votre part ; les autres ont besoin d'une variable
d'environnement, et quand elle manque, ils n'échouent pas. Ils font un
return, et l'exécution signale un succès.
C'est délibéré — cela garde cargo test fonctionnel sur un portable sans
serveur — mais cela veut dire qu'une exécution qui passe n'est pas la preuve que
la suite a tourné. Bon à savoir avant de lire un résultat vert comme de la
couverture.
Quelle variable veut chaque suite
| Variable | Suites | Ce dont elles ont besoin |
|---|---|---|
| (aucune) | 211 | Rien — une SQLite en mémoire ou en fichier temporaire. Tournent toujours. |
DATABASE_URL | 96 | Un serveur PostgreSQL joignable. |
MYSQL_TEST_URL | 28 | Un serveur MySQL 8+ joignable. Pas DATABASE_URL. |
REDIS_TEST_URL | 2 | Un Redis joignable. |
Une suite qui lit deux variables est comptée sous les deux ; la colonne ne totalise donc pas le nombre de fichiers.
Les suites *_tri.rs sont comptées sous les deux variables de serveur. Elles ne
lisent aucune variable elles-mêmes — c'est Backend::pool() qui s'en charge — et
leur bras SQLite tourne sans rien de configuré ; les compter comme « ne demande
rien » serait techniquement tenable et pratiquement faux : les deux bras qui
exigent un serveur sont la raison d'être de ces suites. Démarre les deux
serveurs, sinon une suite tri annoncera un joli total en n'ayant éprouvé qu'un
backend sur trois.
MySQL est celle qui piège les gens : elle lit sa propre variable, donc un shell
où seule DATABASE_URL est définie exécute les suites Postgres et saute
silencieusement toutes les suites MySQL.
Distinguer un saut d'un succès
La plupart des sauts sont un simple return anticipé, sans aucune sortie. Une
minorité affiche d'abord une ligne sur stderr, que cargo test masque à moins
que vous ne la demandiez :
cargo test --test <name> -- --nocapture
Le signal fiable est le compte. Une suite live qui rapporte 0 passed — ou bien
moins que ce que le fichier contient — a sauté.
running 2 tests … 2 passed sans serveur en marche signifie que ces deux tests
ont fait un return anticipé.
Si vous voulez qu'une suite échoue plutôt que de sauter quand son serveur manque, définissez la variable sur une URL délibérément mauvaise : elle échouera alors à la connexion, ce qui est un signal plus bruyant et plus honnête qu'un saut.
Helpers d'assertion de réponse
Pour les valeurs axum::Response brutes (par exemple issues de tower::oneshot), test_assertions
fournit des vérifications lisibles en une ligne pour le statut, le contenu, les
redirections et les cookies :
use rustango::test_assertions::{assert_status_2xx, assert_redirects, assert_cookie_set};
assert_status_2xx(&res);
assert_redirects(&res, "/login?next=/dashboard");
assert_cookie_set(&res, "rustango_session", None);
Également disponibles : assert_status / assert_status_in / assert_status_4xx /
assert_status_5xx, assert_header, assert_content_type,
assert_redirect_chain, assert_cookie_not_set, et assert_messages.
Voir aussi
- ViewSets · Vues HTML — ce sur quoi vous pointez le
TestClient. - Middleware —
TestClientexerce aussi les couches (sans DB, viatower::oneshotdansmiddleware.rs). - Prise en main — l'étape 16 écrit le premier test.
- CLI
manage—make:testgénère un module de test.
