Lezione 5 di 6 · 18 min di lettura
Scrivere test
La cartella test, gleam test e le funzioni che finiscono in _test. Un test che scova un bug nascosto dal modulo 1, e come si legge un test che fallisce.
Ma funziona davvero?
format_cents ci accompagna dal modulo 1. L’abbiamo usata per scontrini, carrelli, pagamenti, e ha sempre stampato quello che ci aspettavamo. Ma l’abbiamo provata solo con gli importi che ci servivano in quel momento. Funziona con 5 centesimi? E con un importo negativo, come un rimborso?
Provarla a mano ogni volta, con un echo qui e uno là, è noioso e si dimentica. Un test è un piccolo programma che controlla automaticamente che una funzione dia il risultato atteso, e che si può rilanciare in un secondo ogni volta che cambi qualcosa.
La cartella test
Ricordi la cartella test/ creata da gleam new nella lezione 1.3? Nel progetto exercises contiene un file, test/exercises_test.gleam:
import gleeunit
pub fn main() -> Nil {
gleeunit.main()
}
// gleeunit test functions end in `_test`
pub fn hello_world_test() {
let name = "Joe"
let greeting = "Hello, " <> name <> "!"
assert greeting == "Hello, Joe!"
}gleeunit è la libreria che esegue i test (è la dev_dependency che trovi in gleam.toml). Le regole sono poche:
- i test stanno in moduli dentro
test/, e il nome del modulo finisce in_test; - ogni test è una funzione pubblica il cui nome finisce in
_test, senza argomenti; - dentro,
assertcontrolla le condizioni: se tutte sono vere, il test passa; se una è falsa, il test fallisce.
Il comando per eseguirli è:
gleam test Compiling exercises
Compiled in 0.22s
Running exercises_test.main
.
1 passed, no failuresOgni puntino è un test passato.
Il primo test vero
Per essere provata da un test, una funzione deve stare in un modulo di src/ ed essere pubblica, perché il test è un altro modulo che la importa. Creiamo src/money.gleam:
import gleam/int
import gleam/string
/// Formats an amount in cents as euros with two decimals: 690 -> "6.90".
pub fn format_cents(cents: Int) -> String {
int.to_string(cents / 100)
<> "."
<> { cents % 100 |> int.to_string |> string.pad_start(2, "0") }
}e i suoi test in test/money_test.gleam:
import money
pub fn format_cents_test() {
assert money.format_cents(690) == "6.90"
}
pub fn format_small_amount_test() {
assert money.format_cents(5) == "0.05"
}
pub fn format_negative_test() {
assert money.format_cents(-150) == "-1.50"
}Tre test: un caso normale, un importo piccolo (servirà lo zero davanti?), un importo negativo. Nomi descrittivi, perché quando un test fallisce il nome è la prima cosa che leggi. E via:
Compiling exercises
Compiled in 0.22s
Running exercises_test.main
...
assert test/money_test.gleam:12
test: money_test.format_negative_test
code: assert money.format_cents(-150) == "-1.50"
left: "-1.-50"
right: "-1.50"
info: Assertion failed.
3 passed, 1 failuresUn bug! Nascosto dal modulo 1, e trovato al primo tentativo.
Leggere un test che fallisce
Il rapporto dice tutto quello che serve:
assert test/money_test.gleam:12: il file e la riga dell’assertfallito;test: quale test (modulo e funzione);code: la condizione che doveva essere vera;lefteright: i valori dei due lati del==. A sinistra quello che la funzione ha davvero restituito, a destra quello che ti aspettavi.
"-1.-50": ecco il problema. Con un importo negativo, -150 / 100 fa -1 e -150 % 100 fa -50 (ricordi la lezione 1.5? la divisione tronca verso lo zero, e il resto ha il segno del dividendo). Il meno compare due volte. La correzione più semplice: se l’importo è negativo, formatta il suo opposto e aggiungi il meno davanti.
import gleam/int
import gleam/string
/// Formats an amount in cents as euros with two decimals: 690 -> "6.90".
pub fn format_cents(cents: Int) -> String {
case cents < 0 {
True -> "-" <> format_cents(-cents)
False ->
int.to_string(cents / 100)
<> "."
<> { cents % 100 |> int.to_string |> string.pad_start(2, "0") }
}
}gleam test Compiling exercises
Compiled in 0.22s
Running exercises_test.main
....
4 passed, no failuresQuattro test (i tre nuovi più quello di esempio), tutti verdi. E da ora in poi, se una modifica a format_cents dovesse rompere uno di questi casi, gleam test te lo direbbe subito.
Quiz
Quale di queste funzioni viene eseguita da gleam test, se sta in test/money_test.gleam?
Test e sviluppo, insieme
I test non servono solo a controllare il codice già scritto. Molti programmatori li scrivono prima: decidono cosa deve fare una funzione scrivendo i test, li guardano fallire (magari su un todo), e poi scrivono il codice finché diventano tutti verdi. È un modo di lavorare che obbliga a pensare ai casi ai bordi prima di esserne sorpresi.
E ricordi il codice di uscita della lezione precedente? gleam test esce con 0 se tutti i test passano e con 1 se qualcuno fallisce. È così che le GitHub Actions create da gleam new (in .github/workflows/) sanno se segnare un progetto in verde o in rosso a ogni modifica.
Esercizio · sul tuo computer
Il test che non passa
Aggiungi a src/money.gleam questa funzione, che legge un importo scritto come "6.90" e restituisce i centesimi:
/// Parses an amount like "6.90" into cents: Ok(690).
pub fn parse_cents(text: String) -> Result(Int, Nil) {
use #(euros_text, cents_text) <- result.try(string.split_once(text, on: "."))
use <- bool.guard(when: string.length(cents_text) != 2, return: Error(Nil))
use euros <- result.try(int.parse(euros_text))
use cents <- result.try(int.parse(cents_text))
Ok(euros * 100 + cents)
}(ti servono anche import gleam/bool e import gleam/result). Poi, in test/money_test.gleam, scrivi un test per ciascuno di questi casi:
"6.90"diventaOk(690);"0.05"diventaOk(5);"six"è unError(Nil);"6.9"è unError(Nil)(i centesimi devono avere due cifre);"-1.50"diventaOk(-150).
Lancia gleam test. Uno dei cinque fallisce: leggi left e right, capisci perché, e correggi parse_cents finché tutti i test passano.
Mostra una soluzione (prima prova da solo!)
Il test che fallisce è l’ultimo: left: Ok(-50), right: Ok(-150). Il meno finisce solo sugli euro: -1 * 100 + 50 fa -50. Una correzione: se il testo comincia con -, leggere il resto e cambiare segno al risultato.
import gleam/bool
import gleam/int
import gleam/result
import gleam/string
/// Formats an amount in cents as euros with two decimals: 690 -> "6.90".
pub fn format_cents(cents: Int) -> String {
case cents < 0 {
True -> "-" <> format_cents(-cents)
False ->
int.to_string(cents / 100)
<> "."
<> { cents % 100 |> int.to_string |> string.pad_start(2, "0") }
}
}
/// Parses an amount like "6.90" into cents: Ok(690).
pub fn parse_cents(text: String) -> Result(Int, Nil) {
case text {
"-" <> rest -> parse_cents(rest) |> result.map(fn(cents) { -cents })
_ -> parse_positive(text)
}
}
fn parse_positive(text: String) -> Result(Int, Nil) {
use #(euros_text, cents_text) <- result.try(string.split_once(text, on: "."))
use <- bool.guard(when: string.length(cents_text) != 2, return: Error(Nil))
use euros <- result.try(int.parse(euros_text))
use cents <- result.try(int.parse(cents_text))
Ok(euros * 100 + cents)
}import money
pub fn format_cents_test() {
assert money.format_cents(690) == "6.90"
}
pub fn format_small_amount_test() {
assert money.format_cents(5) == "0.05"
}
pub fn format_negative_test() {
assert money.format_cents(-150) == "-1.50"
}
pub fn parse_cents_test() {
assert money.parse_cents("6.90") == Ok(690)
}
pub fn parse_small_amount_test() {
assert money.parse_cents("0.05") == Ok(5)
}
pub fn parse_not_a_number_test() {
assert money.parse_cents("six") == Error(Nil)
}
pub fn parse_one_cent_digit_test() {
assert money.parse_cents("6.9") == Error(Nil)
}
pub fn parse_negative_test() {
assert money.parse_cents("-1.50") == Ok(-150)
}gleam test ora risponde 9 passed, no failures (gli otto test di money_test più quello di esempio). Il pattern "-" <> rest della lezione 2.5 separa il segno, e parse_positive, privata, fa il lavoro sui soli numeri positivi.
Ricapitolando
- I test stanno in
test/, in moduli il cui nome finisce in_test, e si lanciano congleam test. - Un test è una funzione
pub, senza argomenti, con il nome che finisce in_test; dentro,assert condizione. - Le funzioni da provare devono essere
pubin un modulo disrc/, importato dal test. - Un test fallito mostra file, riga, codice, e i valori
left(ottenuto) eright(atteso). - Conviene testare un caso normale, i casi ai bordi e i casi d’errore: i bug vivono ai bordi.
- Un errore di compilazione in
test/ferma anchegleam run. gleam testesce con 1 se un test fallisce: è così che i controlli automatici sanno se il progetto è sano.
Nella prossima lezione la sfida del modulo: il robot del modulo 4 impara a leggere i comandi da un testo, e a dire con gentilezza quando non li capisce.