Test-Driven Development no Rails: Unit Tests

11/05/07

Falta apenas mais um modelo para testarmos: são os comentários. Crie o modelo Comment e abra o arquivo de migração "db/migrate/003_create_comments.rb". Ele deve se parecer com isto:

class CreateComments < ActiveRecord::Migration
  def self.up
    create_table :comments do |t|
      t.column :post_id, :integer, :nil => false
      t.column :name, :string, :limit => 100, :nil => false
      t.column :email, :string, :limit => 100, :nil => false
      t.column :url, :string, :limit => 255, :nil => true
      t.column :created_at, :datetime
      t.column :active, :boolean, :default => false, :nil => false
      t.column :body, :text, :nil => false
    end
  end
 
  def self.down
    drop_table :comments
  end
end

Os requisitos para este modelo são:

  • um comentário deve estar associado a um post
  • só é possível comentar em posts que estão com esta opção ativada
  • nome, comentário e email são obrigatórios; a URL é opcional.

Execute a migração e abra o arquivo "test/unit/comment_test.rb". Vamos criar nossos testes. Os testes são muito semelhantes aos criados anteriormente, por isso, irei apenas colocá-los aqui, sem explicações.

require File.dirname(__FILE__) + '/../test_helper'
 
class CommentTest < Test::Unit::TestCase
  fixtures :comments, :posts
  
  def test_should_be_created
    comment = create(:post_id => posts(:ruby_rules).id)
    assert_valid comment
  end
  
  def test_should_be_invalid
    comment = create(:email => nil, :name => nil, :url => nil, :body => nil)
    assert_invalid comment, "Comment shouldn't be created"
  end
  
  def test_should_require_name
    comment = create(:name => nil)
    assert comment.errors.invalid?(:name), ":name should have had an error"
    assert_invalid comment, "Comment shouldn't be created"
  end
  
  def test_should_require_email
    comment = create(:email => nil)
    assert comment.errors.invalid?(:email), ":email should have had an error"
    assert_invalid comment, "Comment shouldn't be created"
  end
  
  def test_should_deny_bad_email
    comment = create(:email => 'bad@format')
    assert comment.errors.invalid?(:email), ":email should be in a valid format"
    assert_invalid comment, "Comment shouldn't be created"
  end
  
  def test_should_require_comment
    comment = create(:body => nil)
    assert comment.errors.invalid?(:body), ":body should have had an error"
    assert_invalid comment, "Comment shouldn't be created"
  end
  
  def test_should_require_post
    comment = create(:post_id => nil)
    assert comment.errors.invalid?(:post_id), ":post_id should have had an error"
    assert_invalid comment, "Comment shouldn't be created"
    
    comment = create(:post_id => 100)
    assert comment.errors.invalid?(:post), "Post doesn't exist so it should be required"
  end
  
  def test_cannot_comment_because_post_is_closed
    comment = create(:post_id => posts(:rails_rules).id)
    assert_invalid comment, "Comment shouldn't be created"
  end
  
  private
    def create(options={})
      Comment.create({
        :email => 'burns@simpsons.com',
        :name => 'Mr Burns',
        :url => 'http://thesimpsons.com/burns/',
        :body => "Get em', Smithers.",
        :post_id => 2
      }.merge(options))
    end
end

Abra também o arquivo "test/fixtures/comments.yml" e adicione as fixtures abaixo:

comment_on_ruby_post:
  id: 1
  name: Bart Simpson
  email: bart@thesimpsons.com
  url: http://thesimpsons.com/bart
  body: Heya!
  post_id: 2
  created_at: <%= Time.now %>
  active: 0

another_comment_on_ruby_post:
  id: 2
  name: Bart Simpson
  email: bart@thesimpsons.com
  url: http://thesimpsons.com/bart
  body: Heya!
  post_id: 2
  created_at: <%= Time.now %>
  active: 0

comment_on_rails_post:
  id: 3
  name: Principal Skinner
  email: skinner@thesimpsons.com
  url: http://thesimpsons.com/skinner
  body: Bart, you'll be in detention forever!
  post_id: 1
  created_at: <%= Time.now %>
  active: 1

Ao executar estes testes teremos muitas falhas e apenas 1 erro. Novamente, o erro está ligado ao relacionamento que não criamos. Para corrigí-lo, altere o modelo Post.

class Post < ActiveRecord::Base
  has_many :comments, :dependent => :destroy
  #[...]
end

Para validar nosso modelo, basta adicionar os validadores abaixo:

class Comment < ActiveRecord::Base
  belongs_to :post
  
  validates_associated :post
  validates_presence_of :post
  validates_presence_of :post_id
  validates_numericality_of :post_id, :only_integer => true
  
  validates_presence_of :name
  validates_presence_of :email
  validates_presence_of :body, :message => "Don't you wanna comment this post?"
  validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
  
  private
    def validate
      if post
        errors.add_to_base("Comments are closed") unless post.allow_comments
      end
    end
end

A única coisa que você pode estranhar é o método validate. Nele, verifico se um post foi passado para, então, checar se o post está aberto para comentário ou não.

Dicas e considerações

Autotest

Se você se irrita em ter que executar manualmente o rake test:units ou acha que testar é muito lento, dê uma olhada no ZenTest, especificamente no autotest. O autotest roda os testes para você automaticamente e o que é melhor, apenas os métodos que foram alterados. Dessa forma, você tem um feedback muito mais rápido.

Para instalá-lo, execute sudo gem install zentest --include-dependencies -y. Depois, basta executar autotest na raíz do seu aplicativo. No nuby on rails tem um screencast mostrando o autotest em funcionamento.

Usando o logger dentro dos métodos de teste

Se você, por alguma razão, quiser uilizar o logger dentro dos Testes de Unidade, adicione o seguinte método como helper de teste.

def logger
  RAILS_DEFAULT_LOGGER
end

Lembre-se que isso também pode ser feito no seu modelo.

Teste, codifique, teste, codifique

Você pode demorar algum tempo até se acostumar em testar antes de codificar, mas acredite, vale muito a pena! Você se torna mais produtivo e seu código terá menos bugs. Quanto mais completo seus testes forem, maior a certeza de que tudo está funcionando como deveria. O caminho contrário — codificar primeiro, testar depois — pode parecer mais fácil no começo, mas à medida que seu código cresce, você acabará esquecendo de testar alguma funcionalidade importante. Se isso acontecer, torça para que não aconteça nenhum bug.

Nossa, seu exemplo não é nada DRY

Não sei se você percebeu mas as mensagens de erro se repente em diversos testes. Fiz isso para deixar o exemplo o mais didático possível. Se isto te incomoda, remova todas as mensagens. Elas são opcionais.

Queima! Ele não criptografou as senhas….

No nosso exemplo, a senha foi armazenada sem nenhum tipo de criptografia. Nunca, jamais, em hipótese alguma, armazene informações importantes como a senha de maneira "raw". Não fiz isso aqui para não complicar.

E para finalizar…

Os seus testes podem ser mais completos que estes que fizemos. Se você não tem idéia do que testar, faça sempre uma lista dos requisitos para resolver o problema em questão. Do ponto de vista técnico, procure por projetos desenvolvidos em Rails e analise a suíte de testes. Com certeza você encontrará muito coisa legal.

A continuação deste artigo será sobre Testes Funcionais. Não tenho a menor idéia de quando irei escrevê-lo, mas espero que não demore. Dúvidas, críticas ou sugestões? Poste um comentário!

Pages: 1 2 3 4

Comentários #


#1 Davis Zanetti Cabral disse:
11 Maio 07, 09:28AM

Muito bom o artigo. Uma dica para visualizar melhor os testes é o uso do RadRails ou então a instalação do gem RedGreen. Dá uma força bacana na hora de visualizar.

Abraço!

#2 TDD on Rails « Coding Dojo Floripa disse:
11 Maio 07, 09:36AM

[...] Vale a pena conferir. [...]

#3 Klaus Paiva disse:
11 Maio 07, 09:44AM

Muito bom (Ferdi)Nando!

Matou a pau no texto! Ficou muito claro e didático. O trio migrate, tests e fixtures (que eu realmente só conhecia por pouco mais do que o nome) agora me parecem muito mais interessantes!

Valeu! :D

#4 Silfar disse:
11 Maio 07, 10:07AM

Muito show, mas me tira uma dúvida:

def test_should_require_longer_password
user = create(:password => 't')
assert user.errors.invalid?(:password), ":password should be 4 characters or longer"
assert_invalid user, "User shouldn't be created"
end

Pq usar as duas linhas:

assert user.errors….
assert_invalid user……

não entendi pq precisa das duas. Pode me explicar ?

#5 Nando Vieira disse:
11 Maio 07, 10:10AM

Silfar, não precisa. Mas é como eu falei: você pode deixar o seu teste tão completo quanto você queira. Paranoicamente, eu poderia ainda ter adicionado uma asserção para verificar se uma mensagem de erro foi definida. Precisa? Teoricamente, não. Mas é tão simples de fazer que eu realmente não me importo. Ah, mencionei que eu uso snippets para meus testes e que eu só preciso colocar "ai + tab"? :)

#6 Eduardo Fiorezi disse:
11 Maio 07, 11:29AM

Nando, parabéns pela excelente abordagem.

Igual vc falou no comentário, com TDD não é necessário cobrir "100%" das situações, mas é bom cobrir todas necessárias naquele momento… Ficar imaginando tudo que pode acontecer é prejudicial e essas milhões de situações imaginadas talvez nunca apareçam.

Espero os próximos…

#7 Shairon disse:
11 Maio 07, 11:59AM

Muito bom. Estou ansioso para ver o de funcionalidade

#8 Luciano Pacheco disse:
11 Maio 07, 02:30PM

def test_should_require_name
comment = create(:name = nil)
assert comment.errors.invalid?(:name), ":name should have had an error"
assert_invalid comment, "Comment shouldn't be created"
end

":name should have had an error"

Nas mensagens não devemos utilizar mensagens mais explicativa, tipo "name can't be blank" ou algo parecido ?

Estou perguntando, pois também estou iniciando nesse mundo de XP e TDD. :)

Valeu pelo artigo. ;)

#9 Nando Vieira disse:
11 Maio 07, 03:06PM

@Luciano: Melhor do que isso, você pode fazer algo assim:

assert comment.errors.invalid?(:name), ":name should have had an error\n#{comment.errors.full_messages.to_sentence}"

Você decide como quer fazer! ;)

#10 Renato Elias disse:
11 Maio 07, 03:29PM

Mandou bem nando ! Vlw, como você me falou no "praianha" ruby é babaca de tão fácil =)

#11 Diogo Lopes disse:
19 Jun 07, 11:23AM

Parabens cara… nao sei como vim parar aqui, mas estou assinando.
Tem muita gente falando que ABC , XCD , ZZZ é bacana, fundamental, etc… Mas tu mostrou na pratica que vale a pena.
E tudo em Ruby, que é melhor ainda. (até hj, só tinha visto TDD em JAVA em pt)

abcs!

#12 rapha disse:
23 Jun 07, 06:28AM

Fantástico velho. Acabei de escrever os testes de um projeto aqui, e realmente facilita a vida. Agora eu fico muito mais seguro de implementar qualquer funcionalidade, porque eu sei que posso testar tudo de novo em um instante. :D

Rails sem tdd? Nem a pau!

#13 Nome do Jogo » Blog Archive » Vamos falar sobre teste... disse:
05 Out 07, 04:36PM

[...] aprender mais sobre como funcionam os teste no Rails você pode clicar aqui e aqui. E lembrem-se: "Quem não testar é mulher do [...]

#14 davi disse:
15 Nov 07, 04:05PM

Excelente artigo, Nando… :-)

Estou usando isso e o Agile Web Development With Rails e estou gostando.

Mas eu vi uma coisa no livro que você não citou (para simplificar?), o Hash default_error_messages do ActiveRecord::Errors:

@@default_error_messages = {
:inclusion => "is not included in the list",
:exclusion => "is reserved",
:invalid => "is invalid",
:confirmation => "doesn't match confirmation",
:accepted => "must be accepted",
:empty => "can't be empty",
:blank => "can't be blank",
:too_long => "is too long (maximum is %d characters)",
:too_short => "is too short (minimum is %d characters)",
:wrong_length => "is the wrong length (should be %d characters)",
:taken => "has already been taken",
:not_a_number => "is not a number"
}

Eu coloquei o seguinte no meu RAILS_ROOT/test/test_helper.rb:

def assert_activerecord_errors(expected, got, message=")
assert_equal ActiveRecord::Errors.default_error_messages[expected], got, message
end

E agora posso testar as mensagens de erro usando um "simples" assert_activerecord_errors :taken, OBJECT.errors.on(:name).

Abraço

#15 Nando Vieira disse:
15 Nov 07, 04:27PM

@davi, como eu altero as mensagens-padrão do Rails através do atributo :messages, eu utilizo outra forma para validar tais mensagens. Dá uma olhada nos meus helpers[1], para ver como ficou!

[1] http://pastie.caboo.se/114671

#16 Simples Idéias. Por Nando Vieira. » Arquivo » Usando... disse:
01 Jun 08, 11:13PM

[...] muito tempo atrás, escrevi um artigo mostrando como testar uma aplicação Rails usando Test::Unit. Muita coisa aconteceu desde então e [...]

#17 Frolim disse:
24 Jul 08, 12:08PM

Ficou muito bom!
Finalmente iniciei testes unitários com a sua explicação.
Vlw!

#18 Test Helpers - ArthurGeek.net disse:
04 Set 08, 01:15AM

[...] e se você ainda não entrou nessa de TDD, dê uma olhada neste artigo escrito pelo Nando Vieira sobre Unit Tests. Muito boa referência em português! Posted in [...]

#19 Test Helpers - ArthurGeek.net disse:
04 Set 08, 12:25PM

[...] e se você ainda não entrou nessa de TDD, dê uma olhada neste artigo escrito pelo Nando Vieira sobre Unit Tests. Muito boa referência em português! Tags: helpers, [...]

Deixe um comentário




Este blog usa o Gravatar.


Não é aceito código HTML:
adicione-o no pastie.caboo.se ou paste.milk-it.net e poste apenas o link.

Se este é seu primeiro comentário, ele terá que ser aprovado antes de ser exibido.