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

Adicionando funcionalidade com desenvolvimento orientado a testes

Agora que temos a lógica de busca em src/lib.rs separada da função main, fica muito mais fácil escrever testes para a funcionalidade central do nosso código. Podemos chamar funções diretamente com vários argumentos e verificar os valores retornados sem precisar invocar o binário pela linha de comando.

Nesta seção, vamos adicionar a lógica de busca ao programa minigrep usando o processo de desenvolvimento orientado a testes (TDD) com os seguintes passos:

  1. Escrever um teste que falha e executá-lo para confirmar que ele falha pelo motivo esperado.
  2. Escrever ou modificar apenas o código suficiente para fazer o novo teste passar.
  3. Refatorar o código que você acabou de adicionar ou alterar e garantir que os testes continuem passando.
  4. Repetir a partir do passo 1.

Embora seja apenas uma entre muitas formas de escrever software, TDD pode ajudar a orientar o design do código. Escrever o teste antes do código que o faz passar ajuda a manter uma boa cobertura de testes ao longo de todo o processo.

Vamos dirigir por testes a implementação da funcionalidade que de fato fará a busca da string de consulta no conteúdo do arquivo e produzirá uma lista das linhas que correspondem à consulta. Adicionaremos essa funcionalidade em uma função chamada search.

Escrevendo um teste que falha

Em src/lib.rs, adicionaremos um módulo tests com uma função de teste, como fizemos no Capítulo 11. A função de teste especifica o comportamento que queremos para search: ela receberá uma consulta e o texto onde será feita a busca, e retornará apenas as linhas do texto que contêm a consulta. A Listagem 12-15 mostra esse teste.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    unimplemented!();
}

// --snip--

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-15: Criando um teste com falha para a função search referente à funcionalidade que queremos ter

Esse teste procura pela string "duct". O texto em que estamos buscando tem três linhas, e apenas uma delas contém "duct"; observe que a barra invertida após a aspa dupla de abertura instrui Rust a não colocar um caractere de nova linha no começo do conteúdo dessa string literal. Verificamos que o valor retornado pela função search contém apenas a linha que esperamos.

Se executarmos esse teste agora, ele falhará porque a macro unimplemented! entra em pânico com a mensagem “not implemented”. De acordo com os princípios de TDD, vamos dar um pequeno passo: adicionar apenas o código necessário para que o teste deixe de entrar em pânico ao chamar a função, definindo search para sempre retornar um vetor vazio, como mostrado na Listagem 12-16. Então o teste deverá compilar e falhar, porque um vetor vazio não corresponde a um vetor contendo a linha "safe, fast, productive.".

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    vec![]
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-16: Definindo o mínimo da função search para que chamá-la não provoque pânico

Agora vamos discutir por que precisamos definir um lifetime explícito 'a na assinatura de search e usar esse lifetime com o argumento contents e com o valor de retorno. Lembre-se do Capítulo 10: os parâmetros de lifetime especificam qual lifetime de argumento está ligado ao lifetime do valor retornado. Neste caso, indicamos que o vetor retornado deve conter string slices que referenciam slices do argumento contents, e não do argumento query.

Em outras palavras, estamos dizendo a Rust que os dados retornados pela função search viverão tanto quanto os dados passados à função pelo argumento contents. Isso é importante! Os dados referenciados por um slice precisam ser válidos para que a referência também seja válida. Se o compilador assumir que estamos criando string slices de query em vez de contents, ele fará a verificação de segurança incorretamente.

Se esquecermos as anotações de lifetime e tentarmos compilar essa função, receberemos o seguinte erro:

$ cargo build
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
 --> src/lib.rs:1:51
  |
1 | pub fn search(query: &str, contents: &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 `contents`
help: consider introducing a named lifetime parameter
  |
1 | pub fn search<'a>(query: &'a str, contents: &'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 não consegue saber qual dos dois parâmetros deve ser conectado à saída, então precisamos informá-la explicitamente. Observe que o texto de ajuda sugere especificar o mesmo parâmetro de lifetime para todos os parâmetros e para o tipo de saída, mas isso está incorreto! Como contents é o parâmetro que contém todo o texto e queremos retornar partes desse texto que correspondem à busca, sabemos que contents é o único parâmetro que deve ser ligado ao valor de retorno usando a sintaxe de lifetimes.

Outras linguagens de programação não exigem que você conecte argumentos e valores de retorno na assinatura, mas essa prática fica mais natural com o tempo. Talvez você queira comparar este exemplo com os exemplos da seção “Validando referências com lifetimes”

do Capítulo 10.

Escrevendo código para fazer o teste passar

No momento, nosso teste falha porque sempre retornamos um vetor vazio. Para corrigir isso e implementar search, nosso programa precisa seguir estes passos:

  1. Iterar por cada linha do conteúdo.
  2. Verificar se a linha contém a string de busca.
  3. Se contiver, adicioná-la à lista de valores que vamos retornar.
  4. Se não contiver, não fazer nada.
  5. Retornar a lista de resultados correspondentes.

Vamos trabalhar em cada um desses passos, começando pela iteração sobre as linhas.

Iterando pelas linhas com o método lines

Rust tem um método útil para iterar por strings linha a linha, chamado apropriadamente de lines, que funciona como mostrado na Listagem 12-17. Observe que isso ainda não compilará.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        // do something with line
    }
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-17: Iterando por cada linha em contents

O método lines retorna um iterador. Falaremos sobre iteradores em profundidade no Capítulo 13. Mas lembre-se de que você já viu esse modo de usar iteradores na Listagem 3-5

, em que usamos um laço `for` com um iterador para executar

código sobre cada item de uma coleção.

Procurando a consulta em cada linha

Em seguida, vamos verificar se a linha atual contém a nossa string de busca. Felizmente, strings têm um método útil chamado contains que faz isso por nós. Adicione uma chamada ao método contains na função search, como mostra a Listagem 12-18. Observe que isso ainda não vai compilar.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        if line.contains(query) {
            // do something with line
        }
    }
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-18: Adicionando funcionalidade para verificar se a linha contém a string em query

Neste momento, estamos montando a funcionalidade por partes. Para o código compilar, precisamos retornar um valor do corpo da função, como indicamos na assinatura.

Armazenando as linhas correspondentes

Para concluir essa função, precisamos de uma maneira de armazenar as linhas correspondentes que queremos retornar. Para isso, podemos criar um vetor mutável antes do laço for e chamar o método push para armazenar cada line no vetor. Depois do laço for, retornamos o vetor, como mostra a Listagem 12-19.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-19: Armazenando as linhas correspondentes para que possamos retorná-las

Agora a função search deve retornar apenas as linhas que contêm query, e nosso teste deve passar. Vamos executá-lo:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... 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-9cd200e5fac0fc94)

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

Nosso teste passou, então sabemos que funciona!

Neste ponto, poderíamos pensar em oportunidades de refatorar a implementação da função de busca mantendo os testes passando e preservando a mesma funcionalidade. O código dessa função não está ruim, mas ainda não aproveita alguns recursos úteis dos iteradores. Voltaremos a este exemplo no Capítulo 13, onde exploraremos iteradores em detalhe e veremos como melhorá-lo.

Agora o programa inteiro já deve funcionar! Vamos testá-lo, primeiro com uma palavra que deve retornar exatamente uma linha do poema de Emily Dickinson: frog.

$ cargo run -- frog poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/minigrep frog poem.txt`
How public, like a frog

Legal! Agora vamos tentar uma palavra que corresponda a várias linhas, como body:

$ cargo run -- body poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!

E, por fim, vamos garantir que não obteremos linha nenhuma quando buscarmos uma palavra que não aparece em lugar nenhum do poema, como monomorphization:

$ cargo run -- monomorphization poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep monomorphization poem.txt`

Excelente! Construímos nossa própria versão mini de uma ferramenta clássica e aprendemos bastante sobre como estruturar aplicações. Também aprendemos um pouco sobre entrada e saída de arquivos, lifetimes, testes e análise de linha de comando.

Para completar este projeto, vamos demonstrar brevemente como trabalhar com variáveis de ambiente e como imprimir em stderr, os dois muito úteis quando você está escrevendo programas de linha de comando.