Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Scrivere un test che fallisce ed eseguirlo per assicurarsi che fallisca per il motivo previsto.
  2. Scrivere o modificare solo il codice necessario per far passare il nuovo test.
  3. Riscrivere il codice appena aggiunto o modificato e assicurarsi che i test continuino a passare.
  4. 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.

File: src/lib.rs
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));
    }
}
Listato 12-15: Creazione di un test che fallisce per la funzione cerca per la funzionalità che vorremmo implementare

Questo 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."

File: src/lib.rs
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));
    }
}
Listato 12-16: Definire solo una parte sufficiente della funzione cerca in modo che chiamarla non provochi panic

Ora 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:

  1. Iterare ogni riga del contenuto.
  2. Verificare che la riga contenga la nostra stringa di query.
  3. In caso affermativo, aggiungerla all’elenco dei valori ritornati.
  4. In caso contrario, non fare nulla.
  5. 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.

File: src/lib.rs
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));
    }
}
Listato 12-17: Iterazione di ogni riga in 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.

File: src/lib.rs
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));
    }
}
Listato 12-18: Aggiunta di funzionalità per verificare se la riga contiene la stringa in 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.

File: src/lib.rs
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));
    }
}
Listato 12-19: Memorizzazione delle righe corrispondenti in modo da poterle restituire

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.