Test-Driven Development no Rails: Unit Tests
11 de Maio de 2007
Nosso exemplo
Nós vamos criar um sistema de blog — muito mais poderoso que o Wordpress :P — totalmente feito em Rails. Então, a primeira coisa que temos que fazer é pensar nos requisitos de nosso projeto. Isso é importante, pois permite ter uma visão melhor do que precisa ser feito. Obviamente, podemos ajustar tais requisitos ao longo do tempo. A princípio, nosso blog deve:
- permitir configurações sobre o autor (nome, email, etc)
- criar posts com resumo
- permitir que usuários postem comentários, informando email, nome e website
Completo, não? :)
Para começar, vamos criar nossa aplicação. Digite o comando rails blog. Nosso projeto será criado e a lista dos arquivos será exibida. Iremos, então, criar nosso banco de dados — MySQL, neste exemplo — tanto de desenvolvimento quanto de testes. Se você não se sente confortável com a linha de comandos, faça da maneira como está acostumado.
~$ mysqladmin -u root create blog_development
~$ mysqladmin -u root create blog_test
Abra o arquivo "config/database.yml" e insira o usuário e senha que terão acesso aos bancos de dados. Meu arquivo se parece com isso:
development:
adapter: mysql
database: blog_development
username: root
password:
socket: /var/run/mysqld/mysqld.sock
test:
adapter: mysql
database: blog_test
username: root
password:
socket: /var/run/mysqld/mysqld.sock
production:
adapter: mysql
database: blog_production
username: root
password:
socket: /var/run/mysqld/mysqld.sock
É muito importante que você defina 2 bancos diferentes para desenvolvimento e testes, uma vez que o banco de dados "testes" é apagado quando estamos testando nossa aplicação.
Quando nosso desenvolvimento é orientado a testes, você inicialmente só cria os modelos e, logo depois, parte para os testes. Controllers? Não, agora. Você só irá criá-los muito mais à frente. Vamos trabalhar inicialmente no modelo "usuário".
~/blog$ script/generate model User
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/user.rb
create test/unit/user_test.rb
create test/fixtures/users.yml
create db/migrate
create db/migrate/001_create_users.rb
O Rails nos permite trabalhar com DDLs muito facilmente através das migrations. Então, neste texto não iremos lidar com SQL diretamente, mas Ruby.
Abra o arquivo "db/migrate/001_create_users.rb". Nossa tabela de usuários terá os campos "name", "email" e "password". Sua migração deverá ser algo como:
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.column :name, :string, :nil => false
t.column :email, :string, :nil => false
t.column :password, :string, :nil => false
end
end
def self.down
drop_table :users
end
end
Execute o comando rake db:migrate para criar a tabela "users".
~/blog$ rake db:migrate
(in /home/nando/blog)
== CreateUsers: migrating =====================================================
-- create_table(:users)
-> 0.0035s
== CreateUsers: migrated (0.0037s) ============================================
Com a tabela criada, podemos meter a mão na massa!
Abra o arquivo "test/unit/user_test.rb", que foi gerado automaticamente quando criamos nosso modelo. Uma das vantagens de se desenvolver em Rails é justamente esta; é tão simples de se criar testes para uma aplicação, com arquivos criados automaticamente, que você deve se sentir envergonhado de não fazê-lo.
Este arquivo possui uma única asserção chamada test_truth. Apesar de parecer inútil, ela ajuda a corrigir algumas configurações do ambiente, como quando o banco de dados de teste não existe, por exemplo.
require File.dirname(__FILE__) + '/../test_helper'
class UserTest < Test::Unit::TestCase
fixtures :users
# Replace this with your real tests.
def test_truth
assert true
end
end
Para rodarmos nossos testes unitários, devemos executar o comando rake test:units. O Ruby irá executar os testes unitários e receberemos uma resposta como esta:
Started
.
Finished in 0.03095 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
Esta resposta é bastante direta e fácil de entender. Cada ponto exibido na tela (logo abaixo da linha "Started") representa um teste que passou. Temos também uma linha que nos diz que foi executado 1 teste, com 1 asserção, mas que não retornou erro ou falha.
O teste que vem por padrão não faz muita coisa, então vamos criar o nosso! Nosso primeiro modelo a ser testado é o User. Alguns testes possíveis são:
- nome, email e senha são obrigatórios
- a senha deve ter no mínimo 6 caracteres
- o e-mail é único
Podemos escrever um teste genérico para ver se o usuário é criado quando não passamos nenhuma informação.
def test_should_be_invalid
user = User.create
assert !user.valid?, "User shouldn't be created"
end
Primeiro, nós criamos um usuário (User.create) sem passar nenhuma informação. Se nosso modelo tivesse uma validação utilizando os métodos disponíveis do ActiveRecord, o método user.valid? retornaria false e nossa aplicação passaria nos testes. Rodando os testes temos uma surpresa:
~/blog$ rake test:units
Started
F
Finished in 0.050156 seconds.
1) Failure:
test_should_be_invalid(UserTest) [./test/unit/user_test.rb:8]:
User shouldn't be created.
<false> is not true.
1 tests, 1 assertions, 1 failures, 0 errors
rake aborted!
Alguma coisa não está funcionando direito! Nosso teste deveria receber false do método valid?, o que não aconteceu. Não se preocupe em fazer o teste passar. Lembre-se que antes devemos criar os outros testes. Vamos, então, criar cada um dos testes em separado.
Não sei se você notou, mas ficou complicado entender a condição assert !user.valid? no teste que criamos. Para estes casos, podemos utilizar helpers, semelhantes ao que utilizamos nas views, mas que aqui são específicos para os testes. Abra o arquivo "tests/test_helper.rb" e adicione os métodos abaixo:
def deny(condition, message='')
assert !condition, message
end
def assert_invalid(record, message='')
deny record.valid?, message
end
O método deny faz a negativa de assert e o método assert_invalid apenas dá uma força, evitando que tenhamos que explicitar o .valid? toda vez. Não se preocupe em verificar se o método valid? existe ou não; nos testes, assumimos um ambiente e ele deve ser verdadeiro e, caso não seja, investigamos as causas do erro que foi apontado para então corrigí-lo.
Troque o método test_should_be_invalid que criamos anteriormente por este que utiliza nossos helpers.
def test_should_be_invalid
user = User.create
assert_invalid user, "User shouldn't be created"
end
Muito melhor, certo? E assim, você vive sem a culpa de ir contra o princípio DRY
Agora, temos que adicionar outros testes. Antes disso, já prevendo mais um pouco de repetição, vamos criar um método chamado create para nos ajudar. É assim que sua classe de testes deve estar neste momento.
require File.dirname(__FILE__) + '/../test_helper'
class UserTest < Test::Unit::TestCase
fixtures :users
def test_should_be_invalid
user = create(:name => nil, :email => nil, :password => nil)
assert_invalid user, "User shouldn't be created"
end
private
def create(options={})
User.create({
:name => "Homer Simpson",
:email => "homer@simpsons.com",
:password => "test"
}.merge(options))
end
end
O método create será responsável por definir os valores padrão para os campos. Assim, não teremos que digitá-los toda vez que quisermos adicionar um teste.
Os outros testes que iremos criar irão verificar as condições impostas lá em cima. Vamos começar pelo teste que verifica se o nome foi informado.
def test_should_require_name
user = create(:name => nil)
assert user.errors.invalid?(:name), ":name should be required"
assert_invalid user, "User shouldn't be created"
end
Não mudou muita coisa do primeiro teste que fizemos. Apenas adicionamos mais uma asserção que verifica se o campo "name" é inválido. No ActiveRecord, temos os métodos validates_* que necessitam do nome do campo; toda vez que uma validação não passa, um erro é adicionado ao campo. Além de verificar se nosso campo possui um erro, poderíamos verificar se uma mensagem também foi definida. A seguinte asserção faz justamente isso.
assert_not_nil user.errors.on(:name), ":name should have had a error message"
E os outros testes:
require File.dirname(__FILE__) + '/../test_helper'
class UserTest < Test::Unit::TestCase
fixtures :users
def test_should_be_invalid
user = create(:name => nil, :email => nil, :password => nil)
assert_invalid user, "User shouldn't be created"
end
def test_should_require_name
user = create(:name => nil)
assert user.errors.invalid?(:name), ":name should be required"
assert_invalid user, "User shouldn't be created"
end
def test_should_require_email
user = create(:email => nil)
assert user.errors.invalid?(:email), ":email should be required"
assert_invalid user, "User shouldn't be created"
end
def test_should_deny_bad_email
user = create(:email => 'bad@format')
assert user.errors.invalid?(:email), ":email should be in a valid format"
assert_invalid user, "User shouldn't be created"
end
def test_should_require_password
user = create(:password => nil)
assert user.errors.invalid?(:password), ":password should be required"
assert_invalid user, "User shouldn't be created"
end
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
def test_should_deny_duplicate_user
user = create
assert_valid user
user = create
assert_invalid user, "User shouldn't be created"
end
private
def create(options={})
User.create({
:name => "Homer Simpson",
:email => "homer@simpsons.com",
:password => "test"
}.merge(options))
end
end
Execute os testes e veja que uma longa lista de erros irá aparecer.
~/blog$ rake test:units
Loaded suite /usr/lib/ruby/1.8/rake/rake_test_loader
Started
F.FFFFFF
Finished in 0.073839 seconds.
1) Failure:
test_should_be_invalid(UserTest)
[./test/unit/../test_helper.rb:29:in `deny'
./test/unit/../test_helper.rb:33:in `assert_invalid'
./test/unit/user_test.rb:8:in `test_should_be_invalid']:
User shouldn't be created.
<false> is not true.
2) Failure:
test_should_deny_bad_email(UserTest) [./test/unit/user_test.rb:25]:
:email should be in a valid format.
<false> is not true.
3) Failure:
test_should_deny_duplicate_user(UserTest)
[./test/unit/../test_helper.rb:29:in `deny'
./test/unit/../test_helper.rb:33:in `assert_invalid'
./test/unit/user_test.rb:46:in `test_should_deny_duplicate_user']:
User shouldn't be created.
<false> is not true.
4) Failure:
test_should_require_email(UserTest) [./test/unit/user_test.rb:19]:
:email should be required.
<false> is not true.
5) Failure:
test_should_require_longer_password(UserTest) [./test/unit/user_test.rb:37]:
:password should be 4 characters or longer.
<false> is not true.
6) Failure:
test_should_require_name(UserTest) [./test/unit/user_test.rb:13]:
:name should be required.
<false> is not true.
7) Failure:
test_should_require_password(UserTest) [./test/unit/user_test.rb:31]:
:password should be required.
<false> is not true.
8 tests, 9 assertions, 7 failures, 0 errors
Foram executados 8 testes, com 9 asserções, sendo que 7 falharam. O único teste que passou foi test_should_create_user, como era de se esperar. O que temos que fazer agora? Criar o código que irá passar nestes testes. No caso dos testes unitários isso é bastante simples. Você trabalha basicamente com modelos, então, abra o arquivo "app/models/user.rb". Você não precisa resolver os testes que falharam na ordem em que foram exibidos. Comece pelo que você julgar ser mais simples e com menor dependência. Que tal começarmos pela falha 4: :email should be required. Esta falha é bastante simples de se resolver, bastando que você coloque o método validates_presence_of no modelo. Por equivalência, também podemos resolver as falhas 6 e 7.
class User < ActiveRecord::Base
validates_presence_of :email
validates_presence_of :name
validates_presence_of :password
end
Execute os testes Agora você verá que 12 asserções foram executadas mas que apenas 3 falharam. Muito mais interessante que o nosso teste anterior!
~/blog$ rake test:units
Loaded suite /usr/lib/ruby/1.8/rake/rake_test_loader
Started
..FF.F..
Finished in 0.065069 seconds.
1) Failure:
test_should_deny_bad_email(UserTest) [./test/unit/user_test.rb:25]:
:email should be in a valid format.
<false> is not true.
2) Failure:
test_should_deny_duplicate_user(UserTest)
[./test/unit/../test_helper.rb:29:in `deny'
./test/unit/../test_helper.rb:33:in `assert_invalid'
./test/unit/user_test.rb:46:in `test_should_deny_duplicate_user']:
User shouldn't be created.
<false> is not true.
3) Failure:
test_should_require_longer_password(UserTest) [./test/unit/user_test.rb:37]:
:password should be 4 characters or longer.
<false> is not true.
8 tests, 12 assertions, 3 failures, 0 errors
Vamos validar o atributo password: ele não deve ter menos que 6 caracteres. Basta adicionar o validador abaixo ao seu modelo.
validates_length_of :password, :minimum => 4
Mais uma vez, execute os testes. Apenas 2 testes falharam: test_should_deny_bad_email e test_should_deny_duplicate_user. Para, finalmente, passar por todos os testes, adicione os métodos abaixo.
validates_uniqueness_of :email, :case_sensitive => false
validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
Ao executar os testes, teremos uma resposta muito mais agradável!
~/blog$ rake test:units
Loaded suite /usr/lib/ruby/1.8/rake/rake_test_loader
Started
........
Finished in 0.082506 seconds.
8 tests, 14 assertions, 0 failures, 0 errors
Sim! Todos os nossos testes passaram e não sei se você percebeu mas o esforço foi praticamente nulo. Agora, seguindo nossos requisitos, iremos implementar os posts.
- Permalink
- Trackback
- Feed dos comentários
- Ao som de: Midtown – You Should Know
Textos escritos por
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, [...]
Primeiro, parabéns pelo post que me fez encarar o uso de TDD.
Gostaria de tirar uma dúvida com algun de vocês que tenham utilizado os helpers sugeridos pelo Nando em http://pastie.caboo.se/114671. É o seguinte:
A) Quando utilizo a rotina da seguinte forma...
test "should_belongs_to_publication" do
publication = create
assert_associated publication, "editor"
end
... acontece a seguinte falha..
1) Failure: test_should_belongs_to_publication(ReviewTest)
[./test/test_helper.rb:93:in `assert_associated'
...
expected but was
<#>.
B) Quando ponho, na criação de publication, um publication_id que não existe, dá certo...
test "should_belongs_to_publication" do
publication = create(:publication_id => 100)
assert_associated publication, "editor"
end
Minha dúvida é se o uso correto é este mesmo da opção B ou tem algo errado?
Perdão por por esta dúvida aqui, é que não encontrei referencias sobre o uso destas rotinas na web.
Eleudson, na documentação tem uma nota sobre isso. Validar a associação não significa que o objeto deve estar presente. Experimente adicionar também na sua validação algo como
validates_presence_of :publication, :publication_id.O model está assim:
class Review true
validates_presence_of :body
end
A mensagem de erro do post anterior ficou incompleta, por isso a repito a seguir:
1) Failure:
test_should_belongs_to_publication(ReviewTest)
[./test/test_helper.rb:93:in `assert_associated'
./test/unit/review_test.rb:14:in `test_should_belongs_to_publication'
...
-nil- expected but was
Publication id: 1322847960, editor_id: 1050302101, title: "The great saga of Naruto", isbn: "333333-GHI", source: "www.manganiponweb.net", media: "DVD", idiom: "English", license: "Creative Common", created_at: "2009-02-20 17:50:01", updated_at: "2009-02-20 17:50:01"
Quando eu não ponho um publication_id que não existe, as mudanças deste campo dentro de assert_associated, exemplo record.send("#{attribute}=", nil), não faz mudar o valor de record.publication, que continua apontando para o objeto criado no metodo create. Daí, a assertiva assert_nil(record.send(relationship)) se torna falsa.
Pelo que você falou, tenho mesmo que criar um objeto sem referencia (ex. :publication_id => 100)?
A qual documentação você se refere?
Grato por sua atenção!
Ops!! Model correto sem maior e menor que.
class Review ActiveRecord::Base
belongs_to :publication
validates_associated :publication
validates_presence_of :publication, :publication_id
validates_numericality_of :publication_id, :only_integer => true
validates_presence_of :body
end
[...] Já neste link ele fala sobre a programação de testes…que todo mundo fala maravilhas mais ninguém explica como fazer. http://simplesideias.com.br/tdd-no-rails-unit-tests/ [...]
MUITO bom, Nando!
Parabéns e MUITO obrigado!
[]s
Deixe um comentário