Qualquer um que tenha começado um projeto usando Rails e pulado diretamente para a implementação, sem se preocupar com testes, levante a mão direita. Muito bem, vejo que todo mundo levantou a mão.
Testes são, ao mesmo tempo, um dos maiores fatores de produtividade em uma aplicação Rails e uma das coisas mais difíceis de se fazer. A tendência, especialmente quando o projeto começo a avançar é sempre não testar tanto quanto se deveria. Isso é um fato tão comum que mesmo notórios projetos livres escritos em Rails tem poucos testes ou testes insuficientes.
Esse artigo tem como objetivo mostrar um pouco sobre como implementar testes unitários simples usando uma classe que não pertence a um modelo da aplicação. A idéia é exibir uma seqüência de passos simples para testar de maneira razoavelmente completa uma classe e desmistificar o processo.
Assim, para começar, podemos supor que estamos criando um jogo e precisamos
implementar a classe chamada Card. Essa é uma classe que representa algo
bem específico em nosso problema de domínio mas que não possui uma tradução
imediata em banco de dados. Antes, é uma classe cujas instâncias sempre
estarão associadas a outras.
CardEssa classe, obviamente, representará uma carta de baralho. Sendo uma classe que representa valores imutáveis, sua implementação depende de alguns cuidados. Não sendo uma classe de modelo de dados no Rails, a maneira mais fácil de criá-la é criar um arquivo chamado card.rb no diretório app/models. Esse arquivo terá um teste correspondente, card_test.rb em test/unit, que é o arquivo que editaremos agora.
require File.dirname(__FILE__) + '/../test_helper' class CardTest < Test::Unit::TestCase end
Esse primeiro teste ainda não faz absolutamente nada. Como temos apenas 52 cartas, um primeito teste poderia ser verificar se conseguimos criar todas essas cartas. Para isso, podemos codificar algo assim:
require File.dirname(__FILE__) + '/../test_helper'
class CardTest < Test::Unit::TestCase
def test_all_possible
SUITS.each do |suit|
FACES.each do |face|
card = Card.new(face, suit)
assert_equal face, card.face
assert_equal suit, card.suit
end
end
end
end
Esse primeiro teste cria a carta correspondente a cada naipe e cada face possível, verificando se o naipe e face da carta criada correspondem aos valores informados externamente. Rodando esse teste, temos o seguinte resultado.
Started
E
Finished in 0.043318 seconds.
1) Error:
test_all_possible(CardTest):
NameError: uninitialized constant CardTest::SUITS
/usr/lib/ruby/gems/1.8/gems/activesupport-1.3.1.5848/lib/active_support/dependencies.rb:478:in 'const_missing'
./test/unit/card_test.rb:6:in `test_all_possible'
1 tests, 0 assertions, 0 failures, 1 errors
Obviamente, o teste falha inicialmente porque não temos definidas as
constantes SUITS e FACES. Tampouco temos a nossa classe
Card. Editando o arquivo card.rb para resolver esses
problemas, podemos fazer o seguinte.
SUITS = [:clubs, :diamonds, :spades, :hearts].freeze
FACES = [:ace, :two, :there, :four, :five, :six, :seven, :eight,
:nine, :ten, :jack, :queen, :king].freeze
class Card
end
Rodando o teste, continuamos com o mesmo problema. Isso acontece
porque o arquivo não está sendo encontrado. A maneira mais simples é
incluí-lo via require no arquivo environment.rb.
Rodando o teste novamente, veremos que o erro muda por não termos definido uma inicialização para a classe. É o que faremos agora.
Como também estamos nos aproximando de uma implementação maior, queremos testar também que não haja possibilidade de criar cartas inválidas. Adicionamos então o seguinte teste:
require File.dirname(__FILE__) + '/../test_helper'
class CardTest < Test::Unit::TestCase
def test_all_possible
SUITS.each do |suit|
FACES.each do |face|
card = Card.new(face, suit)
assert_equal face, card.face
assert_equal suit, card.suit
end
end
end
def test_invalid_cards
assert_raise(InvalidCardError) { Card.new(0, 0) }
assert_raise(InvalidCardError) { Card.new("a", "a") }
assert_raise(InvalidCardError) { Card.new(:ace, 0) }
assert_raise(InvalidCardError) { Card.new(0, :clubs) }
end
end
Esse outro teste garante várias situações possíveis de criação de um carta que se contrapõem as criações válidas do teste anterior.
Podemos agora implementar mais detalhes em nossa classe.
SUITS = [:clubs, :diamonds, :spades, :hearts].freeze
FACES = [:ace, :two, :there, :four, :five, :six, :seven, :eight,
:nine, :ten, :jack, :queen, :king].freeze
class InvalidCardError < StandardError; end
class Card
def initialize(face, suit)
@value = FACES.index(face) + 13 * SUITS.index(suit)
rescue
raise InvalidCardError
end
end
Nessa implementação, vamos utilizar apenas uma variável inteira para
armazenar nossa carta. Considerando que só temos 52 variações
possíveis, podemos guardar o naipe e a face em uma codificação
simples, representando o primeiro naipe com valores entre 0 e 12, o
segundo com valores entre 13 e 25, e assim por diante. Caso o valor
não seja encontrado em um dos arrays que criamos, um exceção será
gerada pelo retorno de nil e capturamos essa exceção para gerar a
nossa mais específica.
Rodando os testes, verificamos que agora só temos um erro nos atributos externos do objeto. Implementamos os mesmos da seguinte forma:
SUITS = [:clubs, :diamonds, :spades, :hearts].freeze
FACES = [:ace, :two, :there, :four, :five, :six, :seven, :eight,
:nine, :ten, :jack, :queen, :king].freeze
class InvalidCardError < StandardError; end
class Card
def initialize(face, suit)
@value = FACES.index(face) + 13 * SUITS.index(suit)
rescue
raise InvalidCardError
end
def face
FACES[@value % 13]
end
def suit
SUITS[@value / 13]
end
end
O resultado é que nosso teste agora passa com o seguinte resultado:
Started .. Finished in 0.047398 seconds. 2 tests, 108 assertions, 0 failures, 0 errors
Temos 108 asserções, sendo que 104 são do primeiro teste, que faz duas verificações para cada uma das 52 cartas, e quatro do segundo teste.
Prosseguindo em nossa implementação, queremos que essa classe não seja
mutável, ou seja, que duas instâncias com o mesmo naipe e face sejam
qualitativamente idênticas. Podemos conseguir isso, e de quebra
comparações entre cartas, usando o mixin Comparable. Mas antes,
precisamos elaborar testes para verificar a acurácia de nossa
implementação.
Nossos testes ficam assim agora:
require File.dirname(__FILE__) + '/../test_helper'
class CardTest < Test::Unit::TestCase
def test_all_possible
SUITS.each do |suit|
FACES.each do |face|
card = Card.new(face, suit)
assert_equal face, card.face
assert_equal suit, card.suit
end
end
end
def test_invalid_cards
assert_raise(InvalidCardError) { Card.new(0, 0) }
assert_raise(InvalidCardError) { Card.new("a", "a") }
assert_raise(InvalidCardError) { Card.new(:ace, 0) }
assert_raise(InvalidCardError) { Card.new(0, :clubs) }
end
def test_comparable
c1 = Card.new(:four, :clubs)
c2 = Card.new(:seven, :hearts)
c3 = Card.new(:ace, :spades)
c4 = Card.new(:seven, :diamonds)
c5 = Card.new(:four, :clubs)
assert c1 == c5
assert c1 < c2
assert c1 < c3
assert c1 < c4
assert c2 > c3
assert c2 > c4
assert c3 > c4
end
end
Testamos várias combinações de operadores que ainda não estão
implementados. Para fazer isso, usaremos, como mencionado acima, o
mixin Comparable.
SUITS = [:clubs, :diamonds, :spades, :hearts].freeze
FACES = [:ace, :two, :there, :four, :five, :six, :seven, :eight,
:nine, :ten, :jack, :queen, :king].freeze
class InvalidCardError < StandardError; end
class Card
include Comparable
def initialize(face, suit)
@value = FACES.index(face) + 13 * SUITS.index(suit)
rescue
raise InvalidCardError
end
def face
FACES[@value % 13]
end
def suit
SUITS[@value / 13]
end
def eql?(card)
@value == card.instance_variable_get(:@value)
end
def <=>(card)
@value <=> card.instance_variable_get(:@value)
end
end
Definimos um método eql?, sobrepondo o original para não comparar a
instância, mas um valor que queremos. Implementamos então o operador
<=>, que é usado por Comparable. Com base nesse operador, o
mixin gera métodos para =, !=, >, >=, < e <=.
Um último método que podemos implementar é um método de classe que retorne todas as classes possíveis em um baralho. Podemos criar um teste para esse método também:
require File.dirname(__FILE__) + '/../test_helper'
class CardTest < Test::Unit::TestCase
def test_all_possible
SUITS.each do |suit|
FACES.each do |face|
card = Card.new(face, suit)
assert_equal face, card.face
assert_equal suit, card.suit
end
end
end
def test_invalid_cards
assert_raise(InvalidCardError) { Card.new(0, 0) }
assert_raise(InvalidCardError) { Card.new("a", "a") }
assert_raise(InvalidCardError) { Card.new(:ace, 0) }
assert_raise(InvalidCardError) { Card.new(0, :clubs) }
end
def test_comparable
c1 = Card.new(:four, :clubs)
c2 = Card.new(:seven, :hearts)
c3 = Card.new(:ace, :spades)
c4 = Card.new(:seven, :diamonds)
c5 = Card.new(:four, :clubs)
assert c1 == c5
assert c1 < c2
assert c1 < c3
assert c1 < c4
assert c2 > c3
assert c2 > c4
assert c3 > c4
end
def test_all_cards
cards = []
SUITS.each do |suit|
FACES.each do |face|
cards << Card.new(face, suit)
end
end
assert_equal cards, Card.all_cards
end
end
A implementação seria algo assim:
SUITS = [:clubs, :diamonds, :spades, :hearts].freeze
FACES = [:ace, :two, :there, :four, :five, :six, :seven, :eight,
:nine, :ten, :jack, :queen, :king].freeze
class InvalidCardError < StandardError; end
class Card
include Comparable
def initialize(face, suit)
@value = FACES.index(face) + 13 * SUITS.index(suit)
rescue
raise InvalidCardError
end
def face
FACES[@value % 13]
end
def suit
SUITS[@value / 13]
end
def eql?(card)
@value == card.instance_variable_get(:@value)
end
def <=>(card)
@value <=> card.instance_variable_get(:@value)
end
def self.all_cards
@@all_cards ||= SUITS.inject([]) { |m, s| FACES.each { |f| m << Card.new(f, s) }; m }
end
end
Note que a implementação é mais complexa que o teste. Poderíamos ter um teste cuja geração de cartas espelhasse exatamente o código de geração. Fazer um teste mais simples e em mais passos é uma garantia a mais de que nossas classes estão funcionando corretamente. Obviamente, há sempre a possibilidade de um erro no teste–embora duas implementações diferentes errando ao mesmo tempo seja algo bem improvável.
Todos os testes passam agora perfeitamente. Assim termina a
implementação inicial da classe Card usando testes para fazer o
trabalho de verificação prévia de implementação.
Como nossa rápida implementação mostra, efetuar os testes antes da implementação é sempre uma maneira muito eficiente de verificar a validade do que estamos desenvolvendo.
Obviamente, se estivéssemos testando uma classe de banco de dados, teríamos
testes para verificar se os dados estão indo e voltando para o banco da maneira
que desejamos e se sua representação está adequada. Um erro comum nesses casos
é testar a própria implementação do ActiveRecord. Um exemplo é testar se os
métodos save, update e delete functionam. Isso é um desperdício completo
de tempo, porque esses métodos são fundamentais e testados pelo próprio Rails.
Os seus testes devem se focar no mostrado acima: detalhes de implementação, ou
seja, lógica interna que não tem a ver com o banco de dados. Representação
só é testada quando há modificações.
Em futuros artigos, exploraremos um pouco mais esse tipo de testes e também como testar as demais partes da aplicação.
Todos os diretos reservados a RubyOnBr. Copyright RubyOnBr .
This site is powered by Radiant CMS.