Test-Driven Development no Rails: Unit Tests
Leia em 22 minutos
Todo mundo fala que Test-Driven Development aumenta sua produtividade, reduz a quantidade de erros do seu código e deixa todo mundo mais feliz. O quem ninguém fala é como fazer isso, quando você não conhece nada de testes. Por isso, resolvi escrever este texto, mostrando o pouco que aprendi nas últimas semanas sobre esse tema.
Test-Driven Development (TDD) — Desenvolvimento Orientado a Testes ou Desenvolvimento Guiado por Testes — é uma técnica de desenvolvimento de software onde primeiro são criados os testes e somente depois é escrito o código necessário para passar por eles. Dessa maneira, você escreverá códigos melhores e, o que é mais importante, muito mais rapidamente. Veja como é o ciclo de TDD, segundo o livro Test-Driven Development by Example, de Kent Back (ISBN-0321146530):
- Crie um teste: Cada nova funcionalidade deve começar com um teste escrito. Este teste deve falhar antes da funcionalidade ser implementada. Você deve conhecer claramente os requisitos e especificações da funcionalidade.
- Execute todos os testes: Você saberá que a rotina de testes está funcionando corretamente e que o novo teste não passou sem que o teste da funcionalidade tenha sido implementado.
- Escreva o código: Escreva o código que irá passar naquele teste que você criou na etapa anterior, sem se preocupar em torná-lo elegante/otimizado. É muito importante que o código implementado reflita somente o teste escrito.
- Execute novamente todos os teste: Se todos os testes passarem, você terá certeza que o código atende todos os requisitos testados e que esta nova funcionalidade não afetou outras partes do sistema.
- Refatore o código: Agora você pode "limpar" o código, se for necessário. Lembre-se de executar os testes constantemente durante esta etapa, pois só assim você saberá se o sistema não foi modificado de maneira incorreta, gerando erros.
Os testes, quando devidamente implementados, oferecem uma certa "garantia" de que a aplicação está funcionando da maneira como deveria.
TDD no Rails
Este texto não tem a pretensão de ser o "guia definitivo" de TDD; ao invés disso, você verá uma abordagem simples e direta do assunto, utilizando Ruby on Rails. Não irei explicar detalhadamente como desenvolver em Rails; para isso você tem outras fontes um tanto quanto completas.
- O que é um teste?
- Teste é um método que contém asserções — segundo o dicionário Houaiss, asserção significa "afirmação categórica" — e que representam um cenário de testes em particular. Um teste só passará caso todas as asserções sejam verdadeiras.
- No Ruby, um teste é um método iniciado por "test"; assim, você pode nomear seu método como "test_", "testing_", "testando_", e por aí vai!
O Rails trabalha com alguns tipos diferentes de testes. Existem os testes unitários que são responsáveis pelos testes de modelos; existem os testes funcionais, responsáveis por testar os controllers; e, por último, temos os testes de integração, responsáveis por testar múltiplas camadas de seu aplicativo e a integração entre elas.
O teste unitário será, provavelmente, o primeiro lugar onde você irá trabalhar em qualquer projeto. Isso acontece porque não é preciso escrever muito código — vou além e digo que não é preciso escrever nenhum código — para se criar tais testes, a não ser o próprio teste.
Quando estamos fazendo TDD, é importante que todos os seus testes iniciais não passem na validação, pois você precisa identificar os itens a serem validados para depois corrigi-los. Você deve também criar pelo menos um teste que passe na validação.
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, :null => false
t.column :email, :string, :null => false
t.column :password, :string, :null => 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.
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, :null => false
t.column :excerpt, :string, :limit => 250, :null => true
t.column :body, :text, :null => false
t.column :created_at, :datetime
t.column :updated_at, :datetime
t.column :allow_comments, :boolean, :default => true, :null => false
t.column :user_id, :integer, :null => 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
boolean
fornil
oufalse
a asserção irá falhar. assert_equal(expected, actual, message)
assert_not_equal(expected, actual, message)
- A asserção irá falhar a menos que
expected
eactual
sejam iguais/diferentes. assert_nil(object, message)
assert_not_nil(object, message)
- A asserção irá falhar a menos que
object
seja/não sejanil
. assert_raise(Exception, ..., message) { block... }
assert_not_raise(Exception, ..., message) { block... }
- A asserção irá falhar a menos que
block
dispare/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
string
seja/não seja correspondente à expressão regularpattern
. assert_valid(record)
- Falha a menos que
record
nã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
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, :null => false
t.column :name, :string, :limit => 100, :null => false
t.column :email, :string, :limit => 100, :null => false
t.column :url, :string, :limit => 255, :null => true
t.column :created_at, :datetime
t.column :active, :boolean, :default => false, :null => false
t.column :body, :text, :null => 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:
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:
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:
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!