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:
- Escrever um teste que falha e executá-lo para confirmar que ele falha pelo motivo esperado.
- Escrever ou modificar apenas o código suficiente para fazer o novo teste passar.
- Refatorar o código que você acabou de adicionar ou alterar e garantir que os testes continuem passando.
- 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.
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));
}
}
search referente à funcionalidade que queremos terEsse 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.".
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));
}
}
search para que chamá-la não provoque pânicoAgora 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:
- Iterar por cada linha do conteúdo.
- Verificar se a linha contém a string de busca.
- Se contiver, adicioná-la à lista de valores que vamos retornar.
- Se não contiver, não fazer nada.
- 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á.
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));
}
}
contentsO 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
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.
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));
}
}
queryNeste 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.
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));
}
}
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.