teste (#[test]) în Rust

Module, macro-uri, atribute și teste Rust 1.0 (#[ignore = "motiv"]: 1.61)

O funcție marcată cu #[test] este un test automat: cargo test o rulează și raportează dacă a trecut sau a dat panică.

Sintaxă
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn nume_test() {
        assert_eq!(functie(2), 4);
    }

    #[test]
    #[should_panic(expected = "text")]
    fn trebuie_sa_dea_panica() { ... }
}

Ce face teste (#[test])

Rust are testele integrate în limbaj. Orice funcție fără parametri marcată cu #[test] este un test: trece dacă se termină normal și eșuează dacă dă panică (de exemplu un assert_eq! neîndeplinit). cargo test compilează proiectul în mod test și rulează toate testele în paralel.

Prin convenție, testele unitare stau în același fișier cu codul, într-un modul #[cfg(test)] mod tests. #[cfg(test)] face ca modulul să fie compilat doar la cargo test, nu și în programul final, iar use super::*; aduce în test funcțiile din fișier (inclusiv cele private). Testele de integrare, care folosesc biblioteca din exterior, se pun în directorul tests/.

#[should_panic] marchează un test care trebuie să dea panică (opțional cu expected = "fragment din mesaj"). #[ignore] sare peste un test lent, care se rulează doar cu cargo test -- --ignored. Un test poate returna și Result<(), E>, ca să folosească ?.

Exemple

Exemplul 1: Teste unitare care trec

Fișierul src/lib.rs al bibliotecii note conține două funcții și testele lor. Ambele teste trec.

// src/lib.rs
pub fn media(note: &[u8]) -> f64 {
    let suma: u32 = note.iter().map(|&n| n as u32).sum();
    suma as f64 / note.len() as f64
}

pub fn calificativ(media: f64) -> &'static str {
    if media >= 9.0 {
        "FB"
    } else if media >= 7.0 {
        "B"
    } else if media >= 5.0 {
        "S"
    } else {
        "I"
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn media_corecta() {
        assert_eq!(media(&[10, 9, 8]), 9.0);
    }

    #[test]
    fn calificative() {
        assert_eq!(calificativ(9.5), "FB");
        assert_eq!(calificativ(7.0), "B");
        assert_eq!(calificativ(4.99), "I");
    }
}

$ cargo test

Rezultat în terminal

   Compiling note v0.1.0 (/home/ana/proiecte/note)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.66s
     Running unittests src/lib.rs (target/debug/deps/note-acd73a4da84441e9)

running 2 tests
test tests::calificative ... ok
test tests::media_corecta ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests note

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Exemplul 2: should_panic, ignore și un test care eșuează

nota_prea_mare trece pentru că funcția dă panica așteptată. toate_notele este sărit. nota_zece are o greșeală în valoarea așteptată, deci eșuează: raportul arată ambele valori (left = cea calculată, right = cea așteptată) și mesajul nostru. Numărul dintre paranteze după numele firului diferă la fiecare rulare.

// src/lib.rs
pub fn nota_din_text(text: &str) -> u8 {
    let n: u8 = text.trim().parse().expect("nota trebuie să fie un număr");
    assert!((1..=10).contains(&n), "nota {n} nu este între 1 și 10");
    n
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn spatii_ignorate() {
        assert_eq!(nota_din_text(" 9\n"), 9);
    }

    #[test]
    #[should_panic(expected = "nu este între 1 și 10")]
    fn nota_prea_mare() {
        nota_din_text("11");
    }

    #[test]
    fn nota_zece() {
        assert_eq!(nota_din_text("10"), 1, "zece trebuie citit ca 10");
    }

    #[test]
    #[ignore = "durează mult"]
    fn toate_notele() {
        for n in 1..=10 {
            assert_eq!(nota_din_text(&n.to_string()), n);
        }
    }
}

$ cargo test

Rezultat în terminal

   Compiling note v0.1.0 (/home/ana/proiecte/note)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.44s
     Running unittests src/lib.rs (target/debug/deps/note-acd73a4da84441e9)

running 4 tests
test tests::toate_notele ... ignored, durează mult
test tests::spatii_ignorate ... ok
test tests::nota_zece ... FAILED
test tests::nota_prea_mare - should panic ... ok

failures:

---- tests::nota_zece stdout ----

thread 'tests::nota_zece' (14557629) panicked at src/lib.rs:24:9:
assertion `left == right` failed: zece trebuie citit ca 10
  left: 10
 right: 1


failures:
    tests::nota_zece

test result: FAILED. 2 passed; 1 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Greșeli frecvente

  • Pui testele într-un modul fără #[cfg(test)]: ele sunt compilate și în programul final, iar importurile folosite doar de teste produc avertismente unused import.
  • Uiți use super::*; în mod tests: funcțiile testate nu sunt găsite (cannot find function), pentru că modulul de teste este un modul separat.
  • Te bazezi pe ordinea testelor sau pe un fișier comun. Testele rulează în paralel și în ordine nedefinită; fiecare test trebuie să fie independent (de exemplu cu nume de fișiere diferite).
  • Ascunzi un test care eșuează cu #[ignore] și uiți de el. Dă mereu un motiv: #[ignore = "așteaptă rezolvarea bug-ului X"].

Comenzi înrudite

  • cargo test: Compilează proiectul în mod test și rulează toate testele: cele #[test], testele din tests/ și exemplele din documentație.
  • assert_eq! și assert_ne!: Verifică dacă două valori sunt egale (assert_eq!) sau diferite (assert_ne!); la eșec, panica afișează ambele valori.
  • assert! și debug_assert!: Verifică o condiție la rulare și dă panică dacă este falsă. debug_assert! face verificarea doar în modul debug, nu și în release.
  • atribute (#[derive], #[cfg], #[allow]): Informații pentru compilator scrise cu #[…] deasupra unui element: generează cod (derive), aleg codul compilat (cfg) sau schimbă avertismentele.
  • module și vizibilitate (mod, pub, crate, super): mod împarte codul în module; tot ce este într-un modul este privat implicit, iar pub îl face vizibil. crate și super încep căile.