Test-Driven Development no Rails: Unit Tests
11/05/07
No modelo Post, devemos escrever testes para validar os seguintes itens:
- um autor pode ter inúmeros posts
- os comentários podem ser permitidos ou não
- o resumo é opcional, mas se for informado não deve ultrapassar 250 caracteres
Como ainda não temos o modelo Post, vamos criá-lo:
script/generate model Post
Abra o arquivo de migração 002_create_posts.rb e adicione o código abaixo.
class CreatePosts < ActiveRecord::Migration
def self.up
create_table :posts do |t|
t.column :title, :string, :limit => 250, :nil => false
t.column :excerpt, :string, :limit => 250, :nil => true
t.column :body, :text, :nil => false
t.column :created_at, :datetime
t.column :updated_at, :datetime
t.column :allow_comments, :boolean, :default => true, :nil => false
t.column :user_id, :integer, :nil => false
end
end
def self.down
drop_table :posts
end
end
O código acima dispensa maiores explicações. Execute o comando rake db:migrate para criarmos a tabela de posts.
~/blog$ script/generate model Post
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/post.rb
create test/unit/post_test.rb
create test/fixtures/posts.yml
exists db/migrate
create db/migrate/002_create_posts.rb
Já podemos criar os testes necessários para validar o modelo de posts. Como nossos testes dependem do modelo User — o post pertece a um autor — temos que carregar alguns usuários no banco de dados. Isso pode ser feito com fixtures.
- O que são fixtures?
- Fixtures são conteúdos de um modelo — ou modelos — que serão carregados no banco de dados para a execução dos testes.
As fixtures podem ser carregadas através de SQL (INSERT INTO ...), arquivos CSV ou, preferencialmente, arquivos YAML. Cada arquivo YAML de conter dados de um único modelo. O nome do arquivo de fixtures deve ser igual ao nome da tabela do banco de dados com a extensão .yml. O Rails cria estes arquivos para você, automaticamente, toda vez que você cria uma migração ou modelo.
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
one:
id: 1
two:
id: 2
O arquivo de fixtures é composto por diversos blocos que são equivalentes a registros do banco de dados. Lembre-se: use tabulação separadas por espaços. Vamos editar o arquivo "test/fixtures/users.yml" para adicionar alguns usuários válidos.
bart:
id: 1
name: Bart Simpson
email: bart@simpsons.com
password: test
krusty:
id: 2
name: Krusty The Clown
email: krusty@simpsons.com
password: test
Agora, abra o arquivo "test/unit/post_test.rb" e carregue as fixtures de usuários.
fixtures :posts, :users
O mais interessante de se utilizar fixtures é que você recebe automaticamente um método com o mesmo nome da tabela de banco de dados e cada registro pode ser acessado pelo nome — bart e krusty, no nosso caso — que você definiu no arquivo de fixtures. Utilize nomes significativos sempre que puder.
Vamos aproveitar e já criar algumas fixtures de posts. Abra o arquivo "test/unit/fixtures/posts.yml" e adicione o texto abaixo.
rails_rules:
id: 1
title: Rails rules
body: Rails is a killer framework built with Ruby
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
user_id: 1
allow_comments: false
ruby_rules:
id: 2
title: Ruby also rules
body: Ruby is a charming language
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
user_id: 1
allow_comments: true
Sim, você pode utilizar código Ruby dentro do arquivo de fixtures! Isso é extramente útil quando você precisa chamar algum método de um modelo (para criptografar a senha, por exemplo) ou trabalhar com datas, como é o nosso caso.
Vamos preparar a nossa classe, adicionando o método create, da mesma maneira que criamos nos testes do modelo User.
require File.dirname(__FILE__) + '/../test_helper'
class PostTest < Test::Unit::TestCase
fixtures :posts, :users
# Replace this with your real tests.
def test_truth
assert true
end
private
def create(options={})
Post.create({
:title => 'Title',
:excerpt => 'Excerpt',
:body => 'Body',
:allow_comments => true,
:user_id => 1
}.merge(options))
end
end
Nossos primeiros teste irão validar os campos obrigatórios.
def test_should_be_invalid
post = create(:title => nil, :excerpt => nil,
:body => nil, :allow_comments => nil, :user_id => nil)
assert_invalid post, "Post shouldn't be created"
end
def test_should_require_title
post = create(:title => nil)
assert post.errors.invalid?(:title), ":title should be required"
assert_invalid post, "Post shouldn't be created"
end
def test_should_require_body
post = create(:body => nil)
assert post.errors.invalid?(:body), ":body should be required"
assert_invalid post, "Post shouldn't be created"
end
def test_should_require_author
post = create(:user_id => nil)
assert post.errors.invalid?(:user_id), ":user_id should be required"
assert_invalid post, "Post shouldn't be created"
end
O resumo pode ter no máximo 250 caracteres mas é opcional. Então vamos aos testes.
def test_should_accept_excerpt
post = create(:excerpt => 'Testing excerpt')
deny post.errors.invalid?(:excerpt), ":excerpt should have been valid"
assert_valid post
end
def test_should_deny_long_excerpt
post = create(:excerpt => "a" * 251)
assert post.errors.invalid?(:excerpt), ":excerpt should have had an error"
assert_invalid post, "Post shouldn't be created"
end
Temos que verificar agora se o usuário existe e se o post foi corretamente associado a ele. Nossos testes:
def test_should_deny_non_integer_user
post = create(:user_id => 'a')
assert post.errors.invalid?(:user_id), ":user_id should have had an error"
assert_invalid post, "Post shouldn't be created"
post = create(:user_id => 1.397)
assert post.errors.invalid?(:user_id), ":user_id should have had an error"
assert_invalid post, "Post shouldn't be created"
end
def test_should_check_post_authorship
# check all fixtures were loaded
assert_equal 2, users(:bart).posts.size, "user should have had 2 posts"
# assign a post without user_id
post = create(:user_id => nil)
# then, assign a post using the relationship method
users(:bart).posts << post
#now, check if user have one more post
assert_equal 3, users(:bart).posts.size, "user should have had 3 posts"
# assign a post to a user that doesn't exist
post = create(:user_id => 100)
assert post.errors.invalid?(:user), "User doesn't exist, so it should be required"
end
E aqui temos um novo método de asserção: assert_equal. Esse método verifica se dois valores são iguais. Veja alguns métodos de asserção que você pode usar.
assert(boolean, message)- Se o parâmetro
booleanforniloufalsea asserção irá falhar. assert_equal(expected, actual, message)assert_not_equal(expected, actual, message)- A asserção irá falhar a menos que
expectedeactualsejam iguais/diferentes. assert_nil(object, message)assert_not_nil(object, message)- A asserção irá falhar a menos que
objectseja/não sejanil. assert_raise(Exception, ..., message) { block… }assert_not_raise(Exception, ..., message) { block… }- A asserção irá falhar a menos que
blockdispare/não dispare um erro da exceção especificada. assert_match(pattern, string, message)assert_no_match(pattern, string, message)- A asserção irá falhar a menos que
stringseja/não seja correspondente à expressão regularpattern. assert_valid(record)- Falha a menos que
recordnão tenha erros de validação.
Na parte dois deste artigo você verá outros métodos de asserção disponíveis para testes dos controllers.
E ao rodarmos os testes unitários, temos…
~/blog$ rake test:units
Loaded suite /usr/lib/ruby/1.8/rake/rake_test_loader
Started
.FEFFFFF.......
Finished in 0.098366 seconds.
1) Failure:
test_should_be_invalid(PostTest)
[./test/unit/../test_helper.rb:30:in `deny'
./test/unit/../test_helper.rb:34:in `assert_invalid'
./test/unit/post_test.rb:9:in `test_should_be_invalid']:
Post shouldn't be created.
<false> is not true.
2) Error:
test_should_check_post_authorship(PostTest):
NoMethodError: undefined method `posts' for #<User:0xb7334d2c>
/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/base.rb:1860:in `method_missing'
./test/unit/post_test.rb:49:in `test_should_check_post_authorship'
3) Failure:
test_should_deny_long_excerpt(PostTest) [./test/unit/post_test.rb:38]:
:excerpt should have had an error.
<false> is not true.
4) Failure:
test_should_deny_non_integer_user(PostTest)
[./test/unit/../test_helper.rb:30:in `deny'
./test/unit/../test_helper.rb:34:in `assert_invalid'
./test/unit/post_test.rb:44:in `test_should_deny_non_integer_user']:
Post shouldn't be created.
<false> is not true.
5) Failure:
test_should_require_author(PostTest) [./test/unit/post_test.rb:26]:
:user_id should be required.
<false> is not true.
6) Failure:
test_should_require_body(PostTest) [./test/unit/post_test.rb:20]:
:body should be required.
<false> is not true.
7) Failure:
test_should_require_title(PostTest) [./test/unit/post_test.rb:14]:
:title should be required.
<false> is not true.
15 tests, 21 assertions, 6 failures, 1 errors
… uma verdadeira catástrofe! Um erro no teste test_should_check_post_authorship nos diz que o método posts não existe. Mas parando para pensar, faz todo sentido, já que nós ainda não definimos o relacionamento entre os modelos. Vamos tratar este erro apenas colocando o relacionamento no modelo User.
class User < ActiveRecord::Base
has_many :posts, :dependent => :destroy
#[...]
end
Note que apenas exibi o código relevante a esta alteração; as validações anteriores permanecem e são representadas aqui por #[...]. Após adicionar esta linha, você já tem o relacionamento entre posts e usuários e se você rodar os testes agora, apenas as falhas serão exibidas.
~/blog$ rake test:units
Loaded suite /usr/lib/ruby/1.8/rake/rake_test_loader
Started
.FFFFFFF........
Finished in 0.142015 seconds.
1) Failure:
test_should_be_invalid(PostTest)
[./test/unit/../test_helper.rb:29:in `deny'
./test/unit/../test_helper.rb:33:in `assert_invalid'
./test/unit/post_test.rb:9:in `test_should_be_invalid']:
Post shouldn't be created.
<false> is not true.
2) Failure:
test_should_check_post_authorship(PostTest) [./test/unit/post_test.rb:63]:
User doesn't exist, so it should be required.
<false> is not true.
3) Failure:
test_should_deny_long_excerpt(PostTest) [./test/unit/post_test.rb:38]:
:excerpt should have had an error.
<false> is not true.
4) Failure:
test_should_deny_non_number_user(PostTest) [./test/unit/post_test.rb:44]:
:user_id should have had an error.
<false> is not true.
5) Failure:
test_should_require_body(PostTest) [./test/unit/post_test.rb:20]:
:body should be required.
<false> is not true.
6) Failure:
test_should_require_title(PostTest) [./test/unit/post_test.rb:14]:
:title should be required.
<false> is not true.
7) Failure:
test_should_require_user(PostTest) [./test/unit/post_test.rb:26]:
:user_id should be required.
<false> is not true.
16 tests, 25 assertions, 7 failures, 0 errors
Vamos às validações mais triviais utilizando o método validates_presence_of.
class Post < ActiveRecord::Base
validates_presence_of :title
validates_presence_of :body
validates_presence_of :user_id
end
~/blog$ rake test:units
Loaded suite /usr/lib/ruby/1.8/rake/rake_test_loader
Started
..FFF...........
Finished in 0.133536 seconds.
1) Failure:
test_should_check_post_authorship(PostTest) [./test/unit/post_test.rb:63]:
User doesn't exist, so it should be required.
<false> is not true.
2) Failure:
test_should_deny_long_excerpt(PostTest) [./test/unit/post_test.rb:38]:
:excerpt should have had an error.
<false> is not true.
3) Failure:
test_should_deny_non_number_user(PostTest) [./test/unit/post_test.rb:44]:
:user_id should have had an error.
<false> is not true.
16 tests, 28 assertions, 3 failures, 0 errors
A coisa já melhorou bastante. As três falhas restantes são relativamente simples de resolver. Primeiro vamos verificar se o user_id é um número.
validates_numericality_of :user_id, :only_integer => true
A falha relativa ao tamanho do resumo pode ser resolvido com uma validação como esta:
validates_length_of :excerpt, :maximum => 250, :if => :check_excerpt?
private
def check_excerpt?
!self.excerpt.blank?
end
E agora, só mais uma falha para corrigir. Estamos ficando bons nisso!
~/blog$ rake test:units
Loaded suite /usr/lib/ruby/1.8/rake/rake_test_loader
Started
..F.............
Finished in 0.149596 seconds.
1) Failure:
test_should_check_post_authorship(PostTest) [./test/unit/post_test.rb:67]:
User doesn't exist, so it should be required.
<false> is not true.
16 tests, 32 assertions, 1 failures, 0 errors
Para corrigir esta falha, você deve primeiro definir que um post está associado a um usuário. Nós fizemos apenas o outro caminho, dizendo que um usuário possui diversos posts. Altere o seu modelo Post, adicionando o relacionamento belongs_to :user. Agora, você poderá adicionar as validações relativas a esta falha.
class Post < ActiveRecord::Base
belongs_to :user
validates_associated :user
validates_presence_of :user
#[..]
end
Perceba que estamos validando a presença do atributo/método user e não user_id. A mesma coisa está sendo feita na segunda parte do teste test_should_check_post_authorship. Isso deve ser feito para se validar a associação entre um post e um usuário, de modo que o usuário deve realmente existir; caso contrário, teriamos uma associação incorreta no teste, já que o usuário com id 100 não existe.
Parabéns! Mais um modelo foi devidamente testado.
~/blog$ rake test:units
Loaded suite /usr/lib/ruby/1.8/rake/rake_test_loader
Started
................
Finished in 0.141916 seconds.
16 tests, 32 assertions, 0 failures, 0 errors
- Permalink
- Trackback
- Feed dos comentários
- Ao som de: Midtown – You Should Know

Comentários #
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!
[...] Vale a pena conferir. [...]
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
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 ?
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"? :)
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…
Muito bom. Estou ansioso para ver o de funcionalidade
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. ;)
@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! ;)
Mandou bem nando ! Vlw, como você me falou no "praianha" ruby é babaca de tão fácil =)
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!
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!
[...] aprender mais sobre como funcionam os teste no Rails você pode clicar aqui e aqui. E lembrem-se: "Quem não testar é mulher do [...]
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
@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
[...] muito tempo atrás, escrevi um artigo mostrando como testar uma aplicação Rails usando Test::Unit. Muita coisa aconteceu desde então e [...]
Ficou muito bom!
Finalmente iniciei testes unitários com a sua explicação.
Vlw!
[...] 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 [...]
[...] 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