https://lafor.ge/feed.xml

Snapshot testing

2023-11-26

Bonjour à toutes et à tous 😀

Vous connaissez surement le dicton "tester c'est douter".

missing alt

Et son pendant "Corriger s'est abdiquer"

missing alt

Bien souvent, l'étape de correction est douloureuse et ou doit être faite rapidement sans prendre le temps de maintenir correctement les tests.

Qui passent parfois dans le pire des cas en commentaires "pour faire passer la CI", ne mentez pas on a tous déjà fait un truc similaire, moi y compris ^^.

Mais pourquoi on se retrouve à déboucler la ceinture de sécurité en plein autoroute en faisant sauter nos tests trop contraignants ?

La réponse est bien souvent que les tests sont réalisés soit à priori via un cahier des charges peut-être écrit des mois avant voir des années, le TDD. Soit à posteriori après que l'application soit totalement terminée et que l'implémentation renvoie un résultat, on lance le test, on voit ce qui sort et on l'utilise comme valeur de consigne pour le test, problème réglé, le code est couvert, testé et tout est dans le meilleur des mondes. 😀

Alors, oui mais quand l'application change, il faut les maintenir ces tests. 😅

Bien souvent c'est là qu'on entend: "je perds plus de temps à écrire des tests qu'à délivrer de la valeur". Et c'est vrai parfois, mais nous sommes développeurs, nous pouvons faire mieux.

Aujourd'hui, je veux vous présenter une crate appellée insta.rs qui tente de régler tout ce travail fastidieux de maintient des tests.

Mais avant ça un peu de contexte

Avant propos

Maintenir ses tests

Commençons avec un projet tout simple. Je considère que vous avez les bases en Rust.

Dans un main.rs

fn main(){}

fn hello<'a>() {
    "hello"
}

#[test]
fn test_hello() {
    assert_eq!(hello(), "hello")
}

Si on fait un coup de cargo test, nous obtenons un succès.

Par contre si l'implémentation évolue, mais que le test reste identique.

fn hello<'a>() {
    "hello world"
}

#[test]
fn test_hello() {
    assert_eq!(hello(), "hello")
}

Alors une erreur survient.

assertion `left == right` failed
  left: "hello world"
 right: "hello"

Left:  hello world
Right: hello

Durant le développement, cela constitue une erreur. Mais après la mise en prod, ça peut très bien être une évolution standard de l'application qui possède désormais un nouveau cahier des charges. Ce n'est donc pas l'implémentation qui est fausse mais le test associé qui n'est plus d'actualité.

Donc soit on modifie le test, soit on le supprime. Nous sommes des personnes de bonnes familles et de bonne éducation, nous allons donc corriger le test.

fn hello<'a>() {
    "hello world"
}

#[test]
fn test_hello() {
    assert_eq!(hello(), "hello world")
}

On voit donc que le test n'est une source de vérité que dans un certain contexte. Il n'est pas immuable, il vit et doit vivre avec l'application.

Un poil d'aléatoire

Dans une application où la sécurité prime, il est bien souvent (et si c'est pas le cas, il faut que ça le soit) nécessaire de recourir à l'aléatoire pour venir générer de la donnée imprédictible.

Prenons l'exemple d'une fonction qui répond à une requête.

J'installe une crate pour avoir ma chaîne aléatoire

cargo add rusty_ulid

Et voici le code

use rusty_ulid::Ulid;

#[derive(Debug, PartialEq)]
struct Request<'a> {
    request_id: Ulid,
    body: &'a str,
}

fn request(body: &str) -> Request {
    Request {
        request_id: Ulid::generate(),
        body,
    }
}

#[test]
fn test_request() {
    assert_eq!(
        request("toto"),
        Request {
            request_id: Ulid::generate(), // comment remplir ça ??
            body: "toto",
        }
    )
}

Et là on tombe sur un os.

assertion `left == right` failed
  left: Request { request_id: Ulid(2056375941257454522078561345809144884), body: "toto" }
 right: Request { request_id: Ulid(2056375941257429071138756606896252643), body: "toto" }

Soit on test comme ça

#[test]
fn test_request() {

    let response = request("toto");

    assert_eq!(response.body, "toto")
}

Mais ce n'est pas très élégant, en plus en négligeant le champ "request_id", il pourrait avoir changé de type ou ne pas avoir les mêmes données.

Bref à éviter.

La deuxième solution est intrusive, elle consiste à rendre l'aléatoire déterministe le temps du test.

Pour cela nous avons besoin d'un aléatoire réglable.

cargo add rand_chacha rand

On se retrouve à outiller le code à tester, en rajoutant les deux paramètres qui la rendent pure. Le temps et l'aléatoire

use rand::{Rng, SeedableRng};
use rusty_ulid::Ulid;

fn request<'a>(body: &'a str, timestamp: u64, mut rng: &mut impl Rng) -> Request<'a> {
    Request {
        request_id: Ulid::from_timestamp_with_rng(timestamp, &mut rng),
        body,
    }
}

Et le code de test doit faire de même.

#[test]
fn test_request() {
    let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
    assert_eq!(
        request("toto", 0, &mut rng),
        Request {
            request_id: Ulid::from_timestamp_with_rng(0, &mut rng),
            body: "toto",
        }
    )
}

Malheureusement

assertion `left == right` failed
  left: Request { request_id: Ulid { value: (29175, 4012448412712807645) }, body: "toto" }
 right: Request { request_id: Ulid { value: (35161, 9941609948520864161) }, body: "toto" }

Alors, on a fait tout ça pour des prunes ? 🥺

Non, car si on relance:

assertion `left == right` failed
  left: Request { request_id: Ulid { value: (29175, 4012448412712807645) }, body: "toto" }
 right: Request { request_id: Ulid { value: (35161, 9941609948520864161) }, body: "toto" }

On voit que la sortie est déterministe, ce qui signifie que notre fonction n'est plus soumise à l'aléatoire.

On va donc pouvoir faire une petite manip'

use std::str::FromStr;

#[test]
fn test_request() {
    let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
    let response = request("toto", 0, &mut rng);
    assert_eq!(
        response,
        Request {
            request_id: Ulid::from_str(&response.request_id.to_string()).unwrap(),
            body: "toto",
        }
    )
}

Alors dans les faits, ça marche, mais quel boulot de mettre ça en place et de le maintenir, de plus la signature autrefois simple s'est complexifié pour permettre de réaliser le test de la fonction.

C'est vraiment pas l'idéal.

Maintenant que le contexte est planté, nous allons pouvoir attaquer le vrai sujet de l'article, la crate insta.rs. 😅

Snapshots

L'idée principale derrière insta est d'outiller le test au lieu d'outiller l'implémentation.

Tout d'abord, installons la crate et sa CLI

cargo add insta --features yaml
cargo install cargo-insta

Vous allez voir tout à l'heure le pourquoi de cette feature "yaml".

Reprenons notre code tout simple.

fn hello<'a>() {
    "hello"
}

Et cette fois-ci au lieu d'utiliser la macro classique assert_eq!, nous allons en utiliser une de insta.

#[test]
fn test_hello() {
    insta::assert_yaml_snapshot!(hello())
}

Si on lance le cargo test, on obtient alors une erreur.

stored new snapshot /data/project/src/snapshots/project__hello.snap.new
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Snapshot Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Snapshot file: /data/project/src/snapshots/project__hello.snap
Snapshot: hello
Source: /data/project:7
───────────────────────────────────────────────────────────────────────────────
Expression: hello()
───────────────────────────────────────────────────────────────────────────────
+new results
────────────┬──────────────────────────────────────────────────────────────────
          0 │+hello
────────────┴──────────────────────────────────────────────────────────────────
To update snapshots run `cargo insta review`

Détaillons un peu ce résultat.

Tout d'abord, nous avons la ligne

stored new snapshot /data/project/src/snapshots/project__hello.snap.new

Que l'on peut visualiser

$ cat /data/project/src/snapshots/project__hello.snap.new
---
source: src/main.rs
assertion_line: 7
expression: hello()
---
hello

Remarquez l'extension .snap.new.

Ok, intéressant, dedans il y a le contenu de la section Snapshot Summary.

Mais surtout, le retour de la fonction hello(). Avec une indication +new results.

───────────────────────────────────────────────────────────────────────────────
Expression: hello()
───────────────────────────────────────────────────────────────────────────────
+new results
────────────┬──────────────────────────────────────────────────────────────────
          0 │+hello
────────────┴──────────────────────────────────────────────────────────────────

Puis finalement une ligne d'indication

To update snapshots run `cargo insta review`

Lançons la commande.

Reviewing [1/1] project@0.1.0:
Snapshot file: src/snapshots/project__hello.snap
Snapshot: hello
Source: /data/project:7
────────────────────────────────────────────────────────────────────────────────
Expression: hello()
────────────────────────────────────────────────────────────────────────────────
+new results
────────────┬───────────────────────────────────────────────────────────────────         
          0 │+hello
────────────┴───────────────────────────────────────────────────────────────────

  a accept     keep the new snapshot
  r reject     reject the new snapshot
  s skip       keep both for now
  i hide info  toggles extended snapshot info
  d hide diff  toggle snapshot diff

On se retrouve devant une commande interactive.

Elle nous propose plusieurs choix:

  • accepter la modification
  • refuser la modification
  • passer la modification

Acceptons la modification en pressant la touche a du clavier.

insta review finished
accepted:
  src/main.rs (hello)

Le fichier devient src/snapshots/project__hello.snap et son contenu

$ cat src/snapshots/project__hello.snap
---
source: src/main.rs
expression: hello()
---
hello

Et là si on relance cargo test, plus aucun problème, le test passe. 😁

Bon Ok, mais peut-être que ça répond toujours vrai.

Essayons de modifier le retour de la méthode hello()

fn hello<'a>() {
    "hello world"
}

#[test]
fn test_hello() {
    insta::assert_yaml_snapshot!(hello())
}

Et cette fois-ci on a bien une erreur.

stored new snapshot /data/project/src/snapshots/project__hello.snap.new
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Snapshot Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Snapshot file: /data/project/src/snapshots/project__hello.snap
Snapshot: hello
Source: /data/project:7
───────────────────────────────────────────────────────────────────────────────
Expression: hello()
───────────────────────────────────────────────────────────────────────────────
-old snapshot
+new results
────────────┬──────────────────────────────────────────────────────────────────
    0       │-hello
          0 │+hello world
────────────┴──────────────────────────────────────────────────────────────────
To update snapshots run `cargo insta review`

Cela ressemble beaucoup à la première erreur excepté que l'on obtient un diff de la nouvelle et ancienne valeur.

On peut alors accepter ou non ces changements au travers de cargo insta review.

On accepte alors les modifications. Et on relance les tests.

De nouveau, le test passe. 😎

Nous venons d'automatiser le processus de mise à jour des tests en fonction de l'évolution de l'implémentation du code.

La CLI de insta permet de décider si l'échec d'un test est une erreur ou une évolution du cahier des charges de l'application.

Inlining

Il peut être intéressant de ne pas stocker les snapshots dans des fichiers.

Insta propose une fonctionnalité permettant de stocker le snapshot dans une chaîne de caractères directement dans le code du test.

#[test]
fn test_hello() {
    insta::assert_yaml_snapshot!(hello(), @"")
}

Remarquez le deuxième paramètre @"".

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Snapshot Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Snapshot: hello
Source: /data/project:7
───────────────────────────────────────────────────────────────────────────────
Expression: hello()
───────────────────────────────────────────────────────────────────────────────
+new results
────────────┬──────────────────────────────────────────────────────────────────
          0 │+---
          1 │+hello world
────────────┴──────────────────────────────────────────────────────────────────
To update snapshots run `cargo insta review`

On accepte et là magie !

#[test]
fn test_hello() {
    insta::assert_yaml_snapshot!(hello(), @r###"
    ---
    hello world
    "###)
}

Le code a changé et le les tests passent. 🤩

La puissances des proc_macro est démentielle. ^^

La commande a modifié le code source pour venir inline les information précédemment contenue le fichier de snapshot.

Serde

Vous vous rappelez lorsque j'ai mis sous le tapis le fait que l'on a rajouté la feature "yaml".

Et bien nous allons l'utiliser à son plein potentiel, cela grâce à la merveilleuse crate serde.

Nous allons seulement utiliser la partie dérivation.

Mais avant ça il nous faut des données plus conséquente que simplement une chaîne de caractères.

Nous allons bien sûr faire dans l'originalité avec une structure

#[derive(Serialize, Deserialize)]
struct Color {
    r: u8
    v: u8,
    b: u8
}

Ainsi qu'une fonction qui retourne une Color.

fn red() -> Color {
    Color {
        r: 255,
        v: 0,
        b: 0
    }
}

Que l'on peut alors tester.

#[test]
fn test_red() {
    insta::assert_yaml_snapshot!(red(), @"")
}

En lançant une fois avec échec puis en acceptant le code devient.

#[test]
fn test_red() {
    insta::assert_yaml_snapshot!(red(), @r###"
    ---
    r: 255
    v: 0
    b: 0
    "###)
}

Et le test passe!

Cela signifie que tout résultat de fonction qui implémente les traits Serialize et Deserialize est testable par insta, sans aucune modification supplémentaire!! ^^

Alors qu'est ce qui se passe dans les coulisses ?

  1. Le résultat de red est sérialisé en YAML
  2. Le contenu de la snapshot est chargé
  3. Les deux sont comparés

Rédactions : gérer l'aléatoire et le temps

Reprenons le code qui nous faisais des misères.

Et on l'adapte pour utiliser insta.

use rusty_ulid::Ulid;
use serde::{Deserialize, Serialize};

#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Request<'a> {
    request_id: Ulid,
    body: &'a str,
}

fn request(body: &str) -> Request {
    Request {
        request_id: Ulid::generate(),
        body,
    }
}

#[test]
fn test_request() {
    insta::assert_yaml_snapshot!(
        request("toto"), @""
    )
}

On lance le test, ça échoue, on accepte, cela donne:

#[test]
fn test_request() {
    insta::assert_yaml_snapshot!(
        request("toto"), @r###"
    ---
    request_id: 01HG63X5YT6N7CC8DF9XHW11QA
    body: toto
    "###
    )
}

On relance, ça ré-échoue, on est donc au point mort ?

Oui et non, insta a d'autres trésors pour nous, une des fonctionnalité se nomme la redaction ou caviardage en bon françois. ^^

Ajoutons la feature

cargo add insta --features redactions
#[test]
fn test_request() {
    insta::assert_yaml_snapshot!(request("toto"), {
        ".request_id" => "[ulid]"
    }, @"");
}

La syntaxe est assez directement compréhensible.

{
    ".request_id" => "[ulid]"
}

Il s'agit d'une map qui vient faire correspondre le chemin du champ et sa valeur de remplacement. le "." initial représentant la racine de la structure.

Cette map est alors placé en second argument et la valeur d'inlining en dernier.

#[test]
fn test_request() {
    insta::assert_yaml_snapshot!(request("toto"), {
        ".request_id" => [ulid]
    }, @r###"
    ---
    request_id: "[ulid]"
    body: toto
    "###);
}

A partir de maintenant, le champ "request_id" vaudra tout le temps "[ulid]".

Le test passera donc. Et ça sans bidouiller la RNG ou le temps !

Et si vraiment vous avez envie, vous pouvez passer une expression déterministe à la rédaction.

use rand::SeedableRng;
#[test]
fn test_request() {
    let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0);
    let request_id = Ulid::from_timestamp_with_rng(0, &mut rng);
    insta::assert_yaml_snapshot!(request("toto"), {
        ".request_id" => request_id.to_string()
    }, @r###"
    ---
    request_id: 00000000007DPBNP606YTRBXV7
    body: toto
    "###);
}

Et que se passe-il avec plusieurs champs et une hiérarchie de structures ?

use std::time::SystemTime;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct File<'a> {
    name: &'a str,
    folder: Folder<'a>,
    creation_date: SystemTime,
}

#[derive(Serialize, Deserialize)]
struct Folder<'a> {
    name: &'a str,
    creation_date: SystemTime,
}

Ainsi qu'une fonction qui retourne un File.

fn file<'a>(file_name: &'a str, folder_name: &'a str) -> File<'a> {
    File {
        name: file_name,
        folder: Folder {
            name: folder_name,
            creation_date: SystemTime::now(),
        },
        creation_date: SystemTime::now(),
    }
}

On a deux source de non déterminisme:

  • File.creation_date
  • Folder.creation_date

On va donc redact ces deux champs.

{
    ".creation_date" => "[file creation date]",
    ".folder.creation_date" => "[folder creation date]"
}

On remarque le nom de la clef suit le chemin du champ dans la hiérarchie de la structure.

En lançant le test, qui échoue, acceptant la snapshot, on obtient:

#[test]
fn test_file() {
    insta::assert_yaml_snapshot!(file("main.rs", "src"), {
      ".creation_date" => "[file creation date]",
      ".folder.creation_date" => "[folder creation date]"
    }, @r###"
    ---
    name: main.rs
    folder:
      name: src
      creation_date: "[folder creation date]"
    creation_date: "[file creation date]"
    "###)
}

Qui règle le problème de déterminisme de SystimeTime::now.

Vérifier avant rédaction

Ok, mais qui dit que le format de la donnée de retour n'a pas changé ? Vu que l'on remplace le contenu du champ, on fait comment pour vérifier les choses ?

Bon, alors reprenons la structure Request et sont test.

#[test]
fn test_request() {
    insta::assert_yaml_snapshot!(request("toto"), {
        ".request_id" => "[request ID]"
    }, @"");
}

Pour combler ce manque, il est possible d'utiliser une autre macro de insta, insta::dynamic_redaction

#[test]
fn test_request() {
    insta::assert_yaml_snapshot!(request("toto"), {
        ".request_id" => insta::dynamic_redaction(|value, _path| {
            let ulid_value = value.as_str().map(Ulid::from_str);
            assert!(ulid_value.is_some());
            assert!(ulid_value.unwrap().is_ok());
            "[request ID]"
        })
    }, @"");
}

Ainsi, vous pouvez faire tous les tests que vous désirez avant de redact la valeur non-déterministe.

Ici, je tente juste de créer un Ulid à partir de la représentation textuelle du champs.

Contexte

Ok, utiliser l'inlining c'est bien mais cela rajoute un nombre conséquent de ligne, le système de fichiers de snapshot est bien plus pratique. Mais pose d'autres soucis.

Voyez plutôt.

Prenons ce code.

struct Data<'a> {
    name: &'a str,
    age: u8,
}

fn template(data: Data) -> String {
    format!("Je m'appelle {}, j'ai {} ans", data.name, data.age)
}

#[test]
fn test_template() {
    let data = Data {
        name: "Noa",
        age: 30,
    };
}
insta::assert_display_snapshot!(template(data));

Après avoir accepté le snapshot on se retrouve ave ce fichier:

---
source: src/main.rs
expression: template(data)
---
Je m'appelle Noa, j'ai 30 ans

Et là on est bien embêté, parce que l'on ne sait plus à quoi correspond data.

Et le problème peut encore s'empirer.

#[test]
fn test_template() {
    let data = Data {
        name: "Noa",
        age: 30,
    };
}
insta::assert_display_snapshot!(template(data));

let data = Data {
        name: "Léa",
        age: 25,
    };
    insta::assert_display_snapshot!(template(data));
}

On se retrouve alors avec deux fichiers de snapshot:

# project__template.snap
---
source: src/main.rs
expression: template(data)
---
Je m'appelle Noa, j'ai 30 ans

Et

# project__template-2.snap
---
source: src/main.rs
expression: template(data)
---
Je m'appelle Léa, j'ai 25 ans

Mouais, pas top, on est obligé de regarder le nom du fichier et compter les méthode de test, puis regarder les argument passer, pour pouvoir analyser la cohérence du test.

Heureusement, insta nous permet d'enrichir cette déclaration.

Insta fourni une macro with_settings qui permet de rajouter le contexte manquant.

#[test]
fn test_template() {
    let data = Data {
        name: "Noa",
        age: 30,
    };

    insta::with_settings!({
        info => &data,
        description => "Doit renvoyer une chaîne avec Noa"
    },{
        insta::assert_display_snapshot!(template(data));
    });

Cela donne, après suppression du snapshot, lancement du test et acceptation du snapshot.

# project__template.snap
---
source: src/template.rs
description: Doit renvoyer une chaîne avec Noa
expression: template(data)
info:
  name: Noa
  age: 30
---
Je m'appelle Noa, j'ai 30 ans

On peut alors dupliqué le test sans être paumé et pourtant avoir assez de contexte pour comprendre ce que l'on lit sans aller voir les sources.

# project__template.snap
---
source: src/template.rs
description: Doit renvoyer une chaîne avec Noa
expression: template(data)
info:
  name: Léa
  age: 30
---
Je m'appelle Léa, j'ai 30 ans

Exemple complet

L'exemple prête à sourire car il est simple, mais imaginez cette structure de données.

use rusty_ulid::Ulid;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize)]
struct Headers {
    headers: HashMap<String, String>,
}

#[derive(Serialize, Deserialize)]
enum Method<'a> {
    Get { path: &'a str },
    Post { path: &'a str },
    Delete { path: &'a str },
}

#[derive(Serialize, Deserialize)]
struct Request<'a> {
    body: Option<&'a str>,
    headers: Headers,
    method: Method<'a>,
}

impl<'a> Request<'a> {
    fn new(method: Method<'a>, headers: Headers, body: Option<&'a str>) -> Self {
        Request {
            request_id: Ulid::generate(),
            body,
            headers,
            method,
        }
    }
}

Avec cette fonction de réponse

#[derive(Serialize, Deserialize)]
enum ResponseStatus {
    Ok,
    Ko,
}

#[derive(Serialize, Deserialize)]
struct Response {
    body: String,
    status: ResponseStatus,
    request_id: Ulid,
}

impl Response {
    fn new(body: &str, status: ResponseStatus) -> Self {
        Self {
            body: body.to_string(),
            status,
        }
    }
}

fn router(request: Request) -> Response {
    match request.method {
        Method::Post { path } => if let "/admin" = path {
            match request.headers.headers.get("x-token") {
                None => Response::new("Token manquant", ResponseStatus::Ko),
                Some(token) => match token.as_str() {
                    "123abc" => Response::new("Bienvenue", ResponseStatus::Ok),
                    _ => Response::new("Vous n'êtes pas autorisé", ResponseStatus::Ko),
                },
            }
        } else {
            Response::new(request.body.unwrap_or_default(), ResponseStatus::Ok)
        },
        _ => Response::new(request.body.unwrap_or_default(), ResponseStatus::Ok),
    }
}

Réalisons une méthode de test

#[test]
fn test_non_admin_route() {
    let request = Request::new(Method::Get { path: "/test" }, Headers::default(), None);
    insta::with_settings!({
        info => &request,
        description => "I s'agit d'un GET sur une route non privilégiée sans body"
    },{
      insta::assert_yaml_snapshot!(router(request))
    });
}

Et là on voit tout de suite l'intérêt ^^

---
source: src/main.rs
description: "Il s'agit d'un GET sur une route non privilégiée sans body"
expression: router(request)
info:
  body: ~
  headers:
    headers: {}
  method:
    Get:
      path: /test
---
body: ""
status: Ok
request_id: "[request ID]"

On peut alors écrire autant de tests que l'on veut, avec autant de snapshots que nécessaire.

Tests
#[test]
fn test_admin_route_no_token() {
    let request = Request::new(
        Method::Post { path: "/admin" },
        Headers::default(),
        Some("delete everything"),
    );
    insta::with_settings!({
        info => &request,
        description => "Il s'agit d'un POST sur une route privilégiée sans token"
    },{
      insta::assert_yaml_snapshot!(router(request), {
            ".request_id" => "[request ID]"
        })
    });
}

#[test]
fn test_admin_route_token_invalid() {
    let headers = Headers {
        headers: HashMap::from([("x-token".to_string(), "456xyz".to_string())]),
    };
    let request = Request::new(
        Method::Post { path: "/admin" },
        headers,
        Some("delete everything"),
    );
    insta::with_settings!({
        info => &request,
        description => "Il s'agit d'un POST sur une route privilégiée avec token invalide"
    },{
      insta::assert_yaml_snapshot!(router(request), {
            ".request_id" => "[request ID]"
        })
    });
}

#[test]
fn test_admin_route_token_ok() {
    let headers = Headers {
        headers: HashMap::from([("x-token".to_string(), "123abc".to_string())]),
    };
    let request = Request::new(
        Method::Post { path: "/admin" },
        headers,
        Some("delete everything"),
    );
    insta::with_settings!({
        info => &request,
        description => "Il s'agit d'un POST sur une route privilégiée avec token correct"
    },{
      insta::assert_yaml_snapshot!(router(request), {
            ".request_id" => "[request ID]"
        })
    });
}
Snaphots
# project__admin_route_no_token.snap
---
source: src/main.rs
description: "Il s'agit d'un POST sur une route privilégiée sans token"
expression: router(request)
info:
  body: delete everything
  headers:
    headers: {}
  method:
    Post:
      path: /admin
---
body: Token manquant
status: Ko
request_id: "[request ID]"
# project__admin_route_token_invalid.snap
---
source: src/main.rs
description: "Il s'agit d'un POST sur une route privilégiée avec token invalide"
expression: router(request)
info:
  body: delete everything
  headers:
    headers:
      x-token: 456xyz
  method:
    Post:
      path: /admin
---
body: "Vous n'êtes pas autorisé"
status: Ko
request_id: "[request ID]"
# project__admin_route_token_ok.snap
---
source: src/main.rs
description: "Il s'agit d'un POST sur une route privilégiée avec token correct"
expression: router(request)
info:
  body: delete everything
  headers:
    headers:
      x-token: 123abc
  method:
    Post:
      path: /admin
---
body: Bienvenue
status: Ok
request_id: "[request ID]"

D'un seul coup d'œil on est capable de déterminer les entrées et les sorties.

Et donc de vérifier la pertinence du test au cours de la vie de l'application.

Conclusion

Insta est une idée géniale, magistralement exécutée, merci à Geal de m'avoir montré ça! 😍

Merci de votre lecture ❤️

avatar

Auteur: Akanoa

Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.

Ce travail est sous licence CC BY-NC-SA 4.0.