quinta-feira, 7 de janeiro de 2010

Como realmente testar: asserções

Falei bastante sobre testes nos dois últimos posts, conceituei testes funcionais e de unidade, falei sobre como criar fixtures, mas faltou a prática de verdade: como usar tudo isso para testar tudo?

Como já disse, testes são conjuntos de asserções. Uma asserção verifica se os parâmetros indicados nela estão válidos, se não estiverem ela retorna uma Falha. O teste executa asserção por asserção, se alguma falhar ele é interrompido, aponta a falha e diz qual das asserções falharam. Se o teste encontrar qualquer erro de sintaxe ou de execução, em vez de retornar uma falha, ele retorna um Erro. Isso já faz ter uma noção simples: caso haja uma Falha, houve erro de lógica, caso haja um Erro, houve erro de codificação.

O exemplo mais básico de asserção é o assert, que verifica se o parâmetro passado é true (lembrem-se que, em Ruby, tudo o que não for nil ou false é verdadeiro.

Porém, apenas com assert não dá para fazer tanta coisa. Sim, podemos ver se uma validação está funcionando corretamente (sim, eu prometo explicar validações), podemos ver se um objeto é igual ao outro usando operadores lógicos, podemos ver se um objeto é instância de determinada classe. Mas fazer tudo isso com assert torna as coisas mais ilegíveis, e é um tanto mais difícil identificar para que serve uma certa asserção. Por isso, existem vários tipos de asserções utilizáveis.

Para aprender outros tipos de asserções, vamos ter alguma ideia de outras funcionalidades para o modelo Pessoa. Digamos que você queira um método que diga o salário líquido da pessoa. Digamos que, caso a pessoa receba menos de 200 reais ela não pague imposto algum. Se o salário for maior ou igual a 200 e menor que 300 ela pague 10% de imposto, se e for 300 ou maior ela pague 20% (impostos tão baixos? Sonho de todo brasileiro...).

Antes de TUDO, vamos abrir aquela fixture para termos dados para testar. Apague tudo o que estiver na fixture e digite isso:

<% for i in (1..4) %>
pessoa_<%= i %>:
  id: <%= i %>
  nome: pes_<%= i %>
  salario: <%= i * 100 %>
  data_nascimento: <%= Date.civil(1980, i, i) %>
<% end %>


Sim, bem parecido com o último exemplo, mas dessa vez há apenas quatro dados.

Agora, ao teste.

Abra o arquivo test/unit/pessoa_test.rb

Insira, abaixo do método definido anteriormente, o seguinte código:

def test_salario_liquido_correto
  assert_equal 100, pessoas(:pessoa_1).salario_liquido
  assert_equal 180, pessoas(:pessoa_2).salario_liquido
  assert_equal 240, pessoas(:pessoa_3).salario_liquido
  assert_equal 320, pessoas(:pessoa_4).salario_liquido
end


Lembram que eu ensinaria como acessar dados da fixture? Aí está. O método tem o mesmo nome do arquivo que guarda os dados (se o arquivo for pessoas.yml, o método é pessoas), e para recuperar um registro específico, você passa como parâmetro o símbolo equivalente à definição do registro.

Por exemplo, no velho exemplo do "programador" (não mude o código da fixture, ok?):

programador:
  id: 1
  nome: Yuri Albuquerque
  salario: 100.00
  data_nascimento: <%= Date.civil(1989, 11, 9) %>

Você acessaria o registro com pessoas(:programador). Sim, simples assim.

Ao fazer isso, você tem uma instância do modelo, e pode chamar qualquer método dele. Por isso o pessoas(:pessoa_1).salario_liquido, eu estou acessando um método da instância do modelo Pessoa.

E atente para a ordem dos parâmetros dada. Ela não é imprescindível, mas, por padrão, o valor esperado vem antes do valor real.

Agora abra o console, vá até o diretório da aplicação e digite

rake


Depois de um tempo você verá que o teste indicou um Erro. E era isso que você deveria esperar, afinal, você tentou acessar um método que não existe (salario_liquido).

Se ele não existe, vamos criá-lo.

Abra o arquivo app/models/pessoa.rb

Lembre-se que esse arquivo define tudo do modelo, seus métodos de instância, seus métodos de classe, suas validações, seus atributos. Se você for um bom programador de Rails, vai fazer boa parte das implementações no Modelo e deixar o controlador cuidar apenas dos redirecionamentos. Não é o meu caso, infelizmente.

Acrescente esse método à classe:

def salario_liquido
  return(salario) if salario < 200
  return(salario * 0.9) if salario >= 200 and salario < 300
  return(salario * 0.8) if salario >= 300
end


Creio que eu não precise explicar o que esse método faz. Rode o rake novamente e veja seu teste apontar um sucesso.

É óbvio que assert e assert_equal não são os únicos métodos dos testes de unidade do Rails. Existem métodos para as mais diversas situações. Vamos a alguns deles:

  • assert_not_equal(obj1, obj2): é o oposto de assert_equal, verifica se os parâmetros dados são diferentes
  • assert_same(obj1, obj2): levemente diferente de assert_equal. Verifica se ambos os objetos têm o mesmo endereço de memória
  • assert_not_same(obj1, obj2): verifica se os objetos não são os mesmos
  • assert_nil(obj): verifica se o objeto está vazio, ou seja, nil
  • assert_not_nil(obj): verifica se o objeto não está vazio
  • assert_match(regex, str): verifica se a string passada combina com a Expressão Regular passada (Expressões Regulares são um assunto comprido, complexo, que enchem livros e livros. Não é o objetivo desse blog detalhá-las).
  • assert_no_match(regex, str): verifica se a string passada não combina com a Expressão Regular passada
  • assert_in_delta(expect, actual, delta): verifica se os números passados estão equivalentes dentro da faixa de tolerância delta dada.
  • assert_throw(symbol) { block }: verifica se o bloco de código "lança" (throw) o símbolo. Blocos de código são uma característica da linguagem Ruby, caso não conheça, sugiro pesquisar sobre eles imediatamente. Mas funciona como passagem de código por "parâmetro".
  • assert_raise(exception1, exception2, ...) { block }: verifica se o bloco de código retorna os erros (exceptions) dados
  • assert_nothing_raised(exception1, exception2, ...) { block }: verifica se o bloco não retorna nenhum dos erros dados
  • assert_instance_of(classe, objeto): verifica se o objeto percence à classe dada.
  • assert_kind_of(classe, objeto): verifica se o objeto percence a alguma classe filha da classe dada.
  • assert_respond_to(objeto, symbol): verifica se o objeto possui o método com o mesmo nome do símbolo.
  • assert_operator(obj1, operator, obj2): verifica se é possível executar obj1.operator(obj2)
  • assert_send(array): verifica se o método listado na array[1], executado no objeto na array[0], com os parâmetros na array[2] em diante retornam true. Sim, bem confuso. Mas não se preocupem muito com esse, por enquanto, particularmente nunca me foi necessário. Tentarei dedicar um post a ele, assim que aprendê-lo exatamente.
  • flunk: sempre retorna uma falha. Bom para indicar testes incompletos.
Lembre-se de testar tudo o que for possível no seu código. Os guias do Ruby on Rails (guias.rubyonrails.pro.br) sugerem fazer pelo menos um teste para cada validação e para cada método. David H. Hansson (de novo o autor de Programando Rails "A Bíblia", vocês vão me ver fazendo várias referências a esse livro) vai ainda mais longe, sugerindo fazer um teste para cada asserção, e pelo menos uma asserção por método. Ele sugere dessa forma porque você saberá exatamente o que está testando, e saberá o que deve corrigir caso algo dê errado.

Particularmente, não uso essa aproximação. Já me basta saber qual asserção falhou quando elas estão agrupadas em testes um pouco mais gerais. Testo mais por situações.

Um bom exemplo é quando lidei com um sistema de autenticação. Fiz um teste para verificar se o usuário logava quando os dados estavam corretos, e outro para verificar se ele NÃO logava quando algum dado estava incorreto. Claro que poderia ter ido um pouco mais longe, e feito um teste para verificar se ele não logava caso o e-mail estivesse incorreto, outro caso a senha estivesse incorreta, outro caso ambos estivessem incorretos, e assim por diante.

Mas agrupar as asserções de testes mais parecidos num único teste não me trouxeram muitos problemas. Com o tempo, você (e sua equipe, claro) vão desenvolver sua própria técnica de testes, e é importante que todos concordem com a forma como tudo está sendo testado.

O importante é programar satisfeito.

quarta-feira, 6 de janeiro de 2010

Para evitar o tédio, fixtures

Duas coisas aconteceriam se você fizesse testes apenas como ensinei no último post: tédio seria a primeira, porque você precisaria criar novas instâncias dos Modelos o tempo todo. A segunda seria a falta de instâncias de Modelos das tabelas que não fazem parte do teste (o que também geraria tédio, já que você precisaria criar cada uma dessas instâncias).

Para resolver isso, existem as fixtures.

Mas o que são fixtures, afinal?

São arquivos YML (Yet another Markup Language, não confundir com XML) que definem instâncias dos modelos. Os arquivos são criados quando você gera os modelos (exatamente como os testes de unidade), mas ficam "vazios", apenas com comentários ensinando a usá-los.

Criar instâncias de uma classe com uma fixture é fácil. Digamos que você queira criar uma instância que você acesse com "programador" (você verá como acessar fixtures mais tarde, então não se preocupe com isso, por enquanto). Tudo o que você precisaria fazer é abrir o arquivo pessoa.yml na pasta test/fixtures, apague tudo o que estiver no arquivo, e digite:



programador:
  id: 1
  nome: Yuri Albuquerque
  salario: 100.00
  data_nascimento: <%= Date.civil(1989, 11, 9) %>



Atente para a identação, ela deve ser dois espaços, e não TAB.

As fixtures são uma maneira bastante concisa de se armazenar dados de testes. Elas são intuitivas para quem vê, são carregadas sempre que os testes são inicializados, e não apresentam muitas dificuldades de acesso.

Porém têm algumas desvantagens. Elas tornam as suítes (conjuntos) de testes lentas; permitem dados inválidos; é complicado manter um grande número de dados em fixtures; e elas são frágeis, no sentido de que uma alteração numa fixture pode afetar o resultado dos seus testes (desvantagens tiradas do livro Programando Rails, "A Bíblia", de David H. Hansson).

Todavia, você pode evitar praticamente tudo com um certo cuidado. Para evitar lentidão, crie apenas fixtures estritamente necessárias (obviamente, isso também evita um grande número de dados). Para evitar dados inválidos, evitem escrever dados inválidos (afinal, você tem total controle sobre o que está numa fixture). Quanto à fragilidade, tome cuidado redobrado com todos os seus testes, o seria uma atitude boa mesmo que você não usasse fixtures.

Finalmente, explicando o código anterior. Boa parte é lógico e intuitivo. A primeira linha indica que toda a identação seguinte será armazenada numa instância "programador". Cada uma das linhas seguintes define um campo da instância.

Mas... o que quer dizer dizer aquele <%= Date.civil(1989, 11, 9) %> ali no meio? É algo bem simples, na verdade. Chama-se expressão. Quando você inicializar as fixtures (o que é feito automaticamente quando você inicia seus testes com rake), o Ruby se encarregará de substituir todo o código entre <%= %> pela string equivalente.

Nesse caso, <%= Date.civil(1989, 11, 9) %> vai ser lido como 1989-11-09.

Mas... por que escrever expressões, quando você pode inserir o dado que você quer?

Lembrem-se: a fixture não filtra dados inválidos. O uso de expressões é uma das muitas formas de evitar a inserção de dados inválidos. Além do quê, muitas vezes o uso de uma expressão é imprescindível, como verá a seguir.

Você deve estar pensando agora "se o Ruby reconhece o código dentro da expressão, eu posso usá-la para fazer um loop e criar várias instâncias de uma vez?". Não. Não pode. A expressão SEMPRE vai ser convertida em string, então tentar executar um loop vai resultar em nil. "Que pena", você deve estar pensando. "Eu gostaria muito de poder gerar dados automaticamente, em vez de digitá-los à exaustão".

E você pode. Só não vai usar expressões para isso, e sim scriplets.

Scriplets são muito parecidos com expressões, inclusive na sintaxe. A principal diferença é que eles NÃO são convertidos em strings, então você pode usá-los para gerenciar códigos e variáveis diretamente no arquivo, e inclusive criar loops.

Um scriplet é definido entre <% %>. Sim, é só tirar o sinal de igual. Vamos dizer que você queira definir cinco fixtures quaisquer de Pessoa. Você pode muito bem, em vez de digitar de uma por uma, fazer isso:


<% for i in (1..5) %>
pessoa_<%= i %>:
  id: <%= i %>
  nome: pes_<%= i %>
  salario: <%= i * 100 %>
  data_nascimento: <%= Date.civil(1980, i, i) %>
<% end %>


Como tudo no Ruby, há mais de uma maneira de escrever a mesma coisa. Minha forma preferida é substituir <% for i in (1..5) %> por <% 1.upto 5 do |i| %>, mas isso não vem ao caso. O que importa é como o Ruby vai enxergar sua fixture depois do processamento, e será assim:


pessoa_1:
  id: 1
  nome: pes_1
  salario: 100
  data_nascimento: 1980-01-01

pessoa_2:
  id: 2
  nome: pes_2
  salario: 200
  data_nascimento: 1980-02-02

pessoa_3:
  id: 3
  nome: pes_3
  salario: 300
  data_nascimento: 1980-03-03

pessoa_4:
  id: 4
  nome: pes_4
  salario: 400
  data_nascimento: 1980-04-04

pessoa_5:
  id: 5
  nome: pes_5
  salario: 500
  data_nascimento: 1980-05-05


Claro que a quantidade de fixtures vai contra a ideia de fazer apenas a quantidade necessária, mas é só um exemplo didático, então não há problemas.

E mais uma dica: lembre-se sempre das expressões e dos scriplets. Eles vão ser SEMPRE necessários, mesmo quando você não estiver fazendo testes