Aggiungere Funzionalità con il Test-Driven Development
Ora che abbiamo la logica di ricerca in src/lib.rs separata dalla funzione
main
, è molto più facile scrivere test per le funzionalità principali del
nostro codice. Possiamo chiamare le funzioni direttamente con vari argomenti e
controllare i valori di ritorno senza dover chiamare il nostro binario dalla
riga di comando.
In questa sezione, aggiungeremo la logica di ricerca al programma minigrep
utilizzando il processo di sviluppo guidato dai test (test-driven
development, abbreviato TDD) con i seguenti passaggi:
- Scrivere un test che fallisce ed eseguirlo per assicurarsi che fallisca per il motivo previsto.
- Scrivere o modificare solo il codice necessario per far passare il nuovo test.
- Riscrivere il codice appena aggiunto o modificato e assicurarsi che i test continuino a passare.
- Ripetere dal passaggio 1!
Sebbene sia solo uno dei tanti modi per scrivere software, il TDD può aiutare a guidare la progettazione del codice. Scrivere il test prima di scrivere il codice che lo supera aiuta a mantenere un’elevata copertura dei test durante l’intero processo.
Testeremo l’implementazione della funzionalità che effettivamente eseguirà la
ricerca della stringa di query nel contenuto del file e produrrà un elenco di
righe che corrispondono alla query. Aggiungeremo questa funzionalità in una
funzione chiamata cerca
.
Scrivere un Test che Fallisce
In src/lib.rs, aggiungeremo un modulo tests
con una funzione di test, come
abbiamo fatto nel Capitolo 11. La funzione di
test specifica il comportamento che vogliamo che abbia la funzione cerca
:
accetterà una query e il testo in cui cercare e ritornerà solo le righe del
testo che contengono la query. Il Listato 12-15 mostra questo test.
pub fn cerca<'a>(query: &str, contenuto: &'a str) -> Vec<&'a str> {
unimplemented!();
}
// --taglio--
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn un_risultato() {
let query = "dut";
let contenuto = "\
Rust:
sicuro, veloce, produttivo.
Scegline tre.";
assert_eq!(vec!["sicuro, veloce, produttivo."], cerca(query, contenuto));
}
}
cerca
per la funzionalità che vorremmo implementareQuesto test cerca la stringa "dut"
. Il testo che stiamo cercando è composto da
tre righe, solo una delle quali contiene "dut"
(nota che la barra rovesciata
dopo le virgolette doppie di apertura indica a Rust di non inserire un carattere
di nuova linea all’inizio del contenuto di questo letterale stringa). Affermiamo
che il valore restituito dalla funzione cerca
contiene solo la riga che ci
aspettiamo.
Se eseguiamo questo test, al momento fallirà perché la macro unimplemented!
si
blocca con il messaggio “not implemented” (non implementato). In conformità
con i principi TDD, aggiungeremo solo il codice necessario per evitare che il
test vada in panico quando si chiama la funzione, definendo la funzione cerca
in modo che ritorni sempre un vettore vuoto, come mostrato nel Listato 12-16.
Quindi il test dovrebbe compilare e fallire perché un vettore vuoto non
corrisponde a un vettore contenente la riga "sicuro, veloce, produttivo."
pub fn cerca<'a>(query: &str, contenuto: &'a str) -> Vec<&'a str> {
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn un_risultato() {
let query = "dut";
let contenuto = "\
Rust:
sicuro, veloce, produttivo.
Scegline tre.";
assert_eq!(vec!["sicuro, veloce, produttivo."], cerca(query, contenuto));
}
}
cerca
in modo che chiamarla non provochi panicOra parliamo del perché è necessario esplicitare la longevità 'a
nella firma
di cerca
e utilizzare tale longevità con l’argomento contenuto
e con il
valore di ritorno. Ricorda che nel Capitolo 10
i parametri di lifetime specificano quale lifetime dell’argomento è
collegata a quella del valore di ritorno. In questo caso, indichiamo che il
vettore restituito deve contenere slice di stringa che fanno riferimento alla
slice dell’argomento contenuto
(piuttosto che all’argomento query
).
In altre parole, diciamo a Rust che i dati restituiti dalla funzione cerca
rimarranno validi finché saranno validi i dati passati alla funzione cerca
nell’argomento contenuto
. Questo è importante! I dati referenziati da una
slice devono essere validi affinché il reference sia valido; se il
compilatore presume che stiamo creando slice di query
anziché di
contenuto
, eseguirà i suoi controlli di sicurezza in modo errato.
Se dimentichiamo le annotazioni di longevità e proviamo a compilare questa funzione, otterremo questo errore:
$ cargo build
Compiling minigrep v0.1.0 (file:///progetti/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:1:51
|
1 | pub fn cerca(query: &str, contenuto: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contenuto`
help: consider introducing a named lifetime parameter
|
1 | pub fn cerca<'a>(query: &'a str, contenuto: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to 1 previous error
Rust non può sapere quale dei due parametri ci serve per l’output, quindi
dobbiamo indicarlo esplicitamente. Nota che il testo di aiuto suggerisce di
specificare lo stesso parametro di longevità per tutti i parametri e il type
di output, il che è sbagliato! Poiché contenuto
è il parametro che contiene
tutto il nostro testo e vogliamo restituire le parti di quel testo che
corrispondono, sappiamo che contenuto
è l’unico parametro che dovrebbe essere
collegato al valore di ritorno utilizzando la sintassi di longevità.
Altri linguaggi di programmazione non richiedono di collegare gli argomenti ai valori di ritorno nella firma, ma questa pratica diventerà più semplice col tempo. Potresti confrontare questo esempio con gli esempi nella sezione “Validare i Reference con la Lifetime” nel Capitolo 10.
Scrivere Codice per Superare il Test
Attualmente, il nostro test fallisce perché restituisce sempre un vettore vuoto.
Per risolvere il problema e implementare cerca
, il nostro programma deve
seguire questi passaggi:
- Iterare ogni riga del contenuto.
- Verificare che la riga contenga la nostra stringa di query.
- In caso affermativo, aggiungerla all’elenco dei valori ritornati.
- In caso contrario, non fare nulla.
- Ritornare l’elenco dei risultati corrispondenti.
Esaminiamo ogni passaggio, iniziando con l’iterazione delle righe.
Iterare le Righe con il Metodo lines
Rust dispone di un metodo utile per gestire l’iterazione riga per riga delle
stringhe, opportunamente chiamato lines
, che funziona come mostrato nel
Listato 12-17. Nota che questo non verrà ancora compilato.
pub fn cerca<'a>(query: &str, contenuto: &'a str) -> Vec<&'a str> {
for line in contenuto.lines() {
// facciamo qualcosa con la riga
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn un_risultato() {
let query = "dut";
let contenuto = "\
Rust:
sicuro, veloce, produttivo.
Scegline tre.";
assert_eq!(vec!["sicuro, veloce, produttivo."], cerca(query, contenuto));
}
}
contenuto
Il metodo lines
restituisce un iteratore. Parleremo degli iteratori in modo
approfondito nel Capitolo 13. Ma ricorda che
hai visto questo modo di usare un iteratore nel Listato 3-5, dove abbiamo usato un ciclo for
con un iteratore per eseguire del
codice su ogni elemento di una collezione.
Ricercare la Query in Ogni Riga
Successivamente, controlleremo se la riga corrente contiene la nostra stringa di
query. Fortunatamente, le stringhe hanno un metodo utile chiamato contains
che
fa proprio questo per noi! Aggiungiamo una chiamata al metodo contains
nella
funzione cerca
, come mostrato nel Listato 12-18. Nota che questo non verrà
ancora compilato.
pub fn cerca<'a>(query: &str, contenuto: &'a str) -> Vec<&'a str> {
for line in contenuto.lines() {
if line.contains(query) {
// facciamo qualcosa con la riga
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn un_risultato() {
let query = "dut";
let contenuto = "\
Rust:
sicuro, veloce, produttivo.
Scegline tre.";
assert_eq!(vec!["sicuro, veloce, produttivo."], cerca(query, contenuto));
}
}
query
Al momento, stiamo sviluppando la funzionalità. Per compilare il codice, dobbiamo restituire un valore dal corpo, come indicato nella firma della funzione.
Memorizzare le Righe Corrispondenti
Per completare questa funzione, abbiamo bisogno di un modo per memorizzare le
righe corrispondenti che vogliamo restituire. Per farlo, possiamo creare un
vettore mutabile prima del ciclo for
e chiamare il metodo push
per
memorizzare una line
nel vettore. Dopo il ciclo for
, ritorniamo il vettore,
come mostrato nel Listato 12-19.
pub fn cerca<'a>(query: &str, contenuto: &'a str) -> Vec<&'a str> {
let mut risultato = Vec::new();
for line in contenuto.lines() {
if line.contains(query) {
risultato.push(line);
}
}
risultato
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn un_risultato() {
let query = "dut";
let contenuto = "\
Rust:
sicuro, veloce, produttivo.
Scegline tre.";
assert_eq!(vec!["sicuro, veloce, produttivo."], cerca(query, contenuto));
}
}
Ora la funzione cerca
dovrebbe restituire solo le righe che contengono
query
, e il nostro test dovrebbe essere superato. Eseguiamo il test:
$ cargo test
Compiling minigrep v0.1.0 (file:///progetti/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 2.28s
Running unittests src/lib.rs (target/debug/deps/minigrep-a16801c2a05e2817)
running 1 test
test tests::un_risultato ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-7b49a695afbb6602)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Il nostro test è stato superato, quindi sappiamo che la funzione fa quello che ci aspettiamo!
A questo punto, potremmo valutare l’opportunità di riscrivere e migliorare l’implementazione della funzione di ricerca, controllando che i test continuino a passare per mantenere la stessa funzionalità. Il codice nella funzione di ricerca non è male, ma non sfrutta alcune utili funzionalità degli iteratori. Torneremo su questo esempio nel Capitolo 13, dove esploreremo gli iteratori in dettaglio e vedremo come migliorarla.
Ora l’intero programma dovrebbe funzionare! Proviamolo, prima con una parola che dovrebbe restituire esattamente una riga della poesia di Emily Dickinson: rana.
$ cargo run -- rana poesia.txt
Compiling minigrep v0.1.0 (file:///progetti/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep rana poesia.txt`
Così volgare — come una rana
Fantastico! Ora proviamo una parola che corrisponda a più righe, come uno:
$ cargo run -- uno poesia.txt
Compiling minigrep v0.1.0 (file:///progetti/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep uno poesia.txt`
Io sono Nessuno! Tu chi sei?
Sei Nessuno anche tu?
Che grande peso essere Qualcuno!
E infine, assicuriamoci di non ottenere alcuna riga quando cerchiamo una parola che non è presente da nessuna parte nella poesia, come monomorfizzazione:
$ cargo run -- monomorfizzazione poesia.txt
Compiling minigrep v0.1.0 (file:///progetti/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorfizzazione poesia.txt`
Ottimo! Abbiamo creato la nostra versione in miniatura di uno strumento classico e abbiamo imparato molto su come strutturare le applicazioni. Abbiamo anche imparato qualcosa sull’input e l’output dei file, sulle lifetime, sui test e sull’analisi della riga di comando.
Per completare questo progetto, mostreremo brevemente come lavorare con le variabili d’ambiente e come stampare su standard error, entrambi utili quando si scrivono programmi da riga di comando.