22
Test-Driven Development no Rails: Unit Tests 11/05/07 Tags: , Ruby , Ruby on Rails , TDD , Teste Unitário , Unit Tests 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): 1. 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. 2. 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. 3. 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. 4. 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. 5. 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.

Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

Embed Size (px)

Citation preview

Page 1: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

Test-Driven Development no Rails: Unit Tests

11/05/07

• Tags: , Ruby, Ruby on Rails, TDD, Teste Unitário, Unit Tests

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):

1. 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.

2. 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.

3. 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.

4. 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.

5. 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.

Page 2: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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

Page 3: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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)

Page 4: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

-> 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

Page 5: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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

Page 6: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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, :pas sword => nil) assert_invalid user, "User shouldn't be created " end private def create(options={}) User.create({ :name => "Homer Simpson", :email => "[email protected]", :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, :pas sword => nil) assert_invalid user, "User shouldn't be created "

Page 7: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

end def test_should_require_name user = create(:name => nil) assert user.errors.invalid?(:name), ":name shou ld 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 sh ould 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 sh ould 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), ":passw ord 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), ":passw ord 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 => "[email protected]", :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_loade r Started F.FFFFFF Finished in 0.073839 seconds.

Page 8: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

1) Failure: test_should_be_invalid(UserTest) [./test/unit/../test_helper.rb:29:in `deny' ./test/unit/../test_helper.rb:33:in `assert_in valid' ./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/u ser_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_in valid' ./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/us er_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/use r_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

Page 9: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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_loade r Started ..FF.F.. Finished in 0.065069 seconds. 1) Failure: test_should_deny_bad_email(UserTest) [./test/unit/u ser_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_in valid' ./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_loade r Started ........ Finished in 0.082506 seconds. 8 tests, 14 assertions, 0 failures, 0 errors

Page 10: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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, :nil => false t.column :excerpt, :string, :limit => 250, :n il => 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.

Page 11: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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: [email protected] password: test krusty: id: 2 name: Krusty The Clown email: [email protected] 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 %>

Page 12: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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_i d => 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 shou ld 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

Page 13: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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 sh ould 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 sh ould have had 2 posts" # assign a post without user_id post = create(:user_id => nil) # then, assign a post using the relationship meth od users(:bart).posts << post #now, check if user have one more post assert_equal 3, users(:bart).posts.size, "user sh ould 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 for nil ou false a asserção irá falhar.

assert_equal(expected, actual, message) assert_not_equal(expected, actual, message)

Page 14: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

A asserção irá falhar a menos que expected e actual sejam iguais/diferentes. assert_nil(object, message) assert_not_nil(object, message)

A asserção irá falhar a menos que object seja/não seja nil . 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 regular pattern .

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_loade r 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_in valid' ./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_mi ssing' ./test/unit/post_test.rb:49:in `test_should_che ck_post_authorship' 3) Failure: test_should_deny_long_excerpt(PostTest) [./test/uni t/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_in valid' ./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/p ost_test.rb:26]: :user_id should be required.

Page 15: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

<false> is not true. 6) Failure: test_should_require_body(PostTest) [./test/unit/pos t_test.rb:20]: :body should be required. <false> is not true. 7) Failure: test_should_require_title(PostTest) [./test/unit/po st_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_loade r 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_in valid' ./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/uni t/post_test.rb:38]: :excerpt should have had an error. <false> is not true. 4) Failure:

Page 16: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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/pos t_test.rb:20]: :body should be required. <false> is not true. 6) Failure: test_should_require_title(PostTest) [./test/unit/po st_test.rb:14]: :title should be required. <false> is not true. 7) Failure: test_should_require_user(PostTest) [./test/unit/pos t_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_loade r 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/uni t/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

Page 17: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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_loade r 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_loade r Started ................ Finished in 0.141916 seconds. 16 tests, 32 assertions, 0 failures, 0 errors

Page 18: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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 c reated" end def test_should_require_name comment = create(:name => nil) assert comment.errors.invalid?(:name), ":name s hould have had an error" assert_invalid comment, "Comment shouldn't be c reated" end def test_should_require_email comment = create(:email => nil)

Page 19: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

assert comment.errors.invalid?(:email), ":email should have had an error" assert_invalid comment, "Comment shouldn't be c reated" 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 c reated" end def test_should_require_comment comment = create(:body => nil) assert comment.errors.invalid?(:body), ":body s hould have had an error" assert_invalid comment, "Comment shouldn't be c reated" end def test_should_require_post comment = create(:post_id => nil) assert comment.errors.invalid?(:post_id), ":pos t_id should have had an error" assert_invalid comment, "Comment shouldn't be c reated" comment = create(:post_id => 100) assert comment.errors.invalid?(:post), "Post do esn'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 c reated" end private def create(options={}) Comment.create({ :email => '[email protected]', :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: [email protected] url: http://thesimpsons.com/bart body: Heya! post_id: 2 created_at: <%= Time.now %> active: 0 another_comment_on_ruby_post: id: 2

Page 20: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

name: Bart Simpson email: [email protected] 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: [email protected] 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 y ou 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") u nless 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

Page 21: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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.

Page 22: Test-Driven Development no Rails: Unit Testshomes.dcc.ufba.br/~mauricio052/Engenharia de Software I/TDD/Test...Test-Driven Development no Rails: Unit Tests 11/05/07 • Tags: , Ruby

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!