sexta-feira, 18 de março de 2011

Ruby on Rails do zero, Teste de model com Rspec

  Continuando nosso blog(github.com/vagnerzampieri/zblog), irei de Rspec hoje para validar nossos models.
 
  Rspec:
 
  Vamos fazer então nossos primeiros teste, algo bem simples, vai ser teste de validação de modelo. Certifique que as gems abaixo estão no seu Gemfile:
 
  gem 'rspec'
  gem 'rspec-rails'
 
  Se não existirem, coloque e faça um "bundle install" no terminal. Ainda no terminal instale o rspec no seu projeto fazendo um:
 
  rails g rspec:install
 
  Ele criará isso:
 
  create  .rspec
  create  spec
  create  spec/spec_helper.rb
 
  Entre na pasta "spec" que ele criou, crie uma pasta chamada "models", dentro dela crie o arquivo "post_spec.rb", e adicione o conteúdo abaixo:
 
  require 'spec_helper'

  describe Post do
    it "test"
  end
 
  Primeiro eu dou um "require 'spec_helper'" para ele chamar o arquivo de mesmo nome que está na pasta "spec" para que eu possa utilizar o Rspec. Eu vou testar comportamento e não método, vou usar BDD, testar comportamento e verificar se a ação que o usuário efetuou está de acordo. Para você ver alguma saída rode no terminal:
 
  rake spec:models
 
  Vocé irá ver uma saída em amarelo, e um contador avisando que existe 1 exemplo, e ele está pendente. Isso é só um teste para ver se o Rspec está funcionando da forma certa. A primeira coisa que testaremos é se o Post pode ser criado sem título, sempre tenha em mente, "red", "green", "refactory".
 
  RED, vamos ver nossa ideia quebrar de propósito com esse exemplo abaixo:
 
  describe Post do
    it "should be not created without title" do
      post = Post.new(:title => "", :body => "Texto qualquer em body", :publication => Time.now.strftime("%Y-%m-%d %H:%M:%S"), :enabled => 1)
      post.should be_valid
    end
  end
 
  Saída:
 
  F

  Failures:

    1) Post should be not created without title
       Failure/Error: post.should be_valid
         expected valid? to return true, got false
       # ./spec/models/post_spec.rb:6:in `block (2 levels) in <top (required)>'

  Finished in 0.04851 seconds
  1 example, 1 failure
  rake aborted!
  ruby -S bundle exec rspec ./spec/models/post_spec.rb failed
 
  Pra quem não conhece Rspec, eu descrevou uma situação quem não pode acontecer no "it", mas faço ao contrário no final "post.should be_valid", eu digo que ele "deve validar". Se você ler a saída vai ver o que estou dizendo. Para nossa espectativa funcionar troque o "should", por "should_not" e irá ver que a saída será GREEN. Faça o mesmo teste para "body", já que são os únicos que validamos no post passado em "app/models/post.rb".
 
  describe Post do
    it "should be not created without title" do
      post = Post.new(:title => "", :body => "Text for body", :publication => Time.now.strftime("%Y-%m-%d %H:%M:%S"), :enabled => 1)
      post.should_not be_valid
    end
   
    it "should be not created without body" do
      post = Post.new(:title => "Title", :body => "", :publication => Time.now.strftime("%Y-%m-%d %H:%M:%S"), :enabled => 1)
      post.should be_valid
    end
  end
 
  Ocorreu o mesmo RED de antes, faça ele ficar GREEN e vamos agora para o REFACTORY. Esse código está meio repetitivo, vamos deixar ele mais elegante:
 
  describe Post do
    before(:each) do
      @post = Post.new(:title => "Title", :body => "Text for body", :publication => Time.now.strftime("%Y-%m-%d %H:%M:%S"), :enabled => 1)
    end
   
    it "should be not created without title" do
      @post.title = nil
      @post.should_not be_valid
    end
   
    it "should be not created without body" do
      @post.body = nil
      @post.should_not be_valid
    end
  end
 
  Eu criei um "before(:each)" que criará um post, dentro de cada "it" eu passo o valor que eu quero testar como "nil", irá dar o efeito que quero. Crio um último "it" para todos os dados passem.
 
  it 'should be created with all requirements' do
    @post.should be_valid
  end
 
  Agora façam os mesmos tipos de teste com User, depois conferem se ficou igual ao que eu fiz. Lembre que o Devise faz validação de campos e que você só precisa testar os campos que exigem validação. E também não existe um certo, e sim o que você acha necessário.
 
  describe User do
    before(:each) do
      @user = User.new(:name => 'User Test', :login => 'usertest', :email => 'test@test.com', :password => 'senha123', :password_confirmation => 'senha123' )
    end
   
    it 'should be not created without name' do
      @user.name = nil
      @user.should_not be_valid
    end
   
    it 'should be not created without login' do
      @user.login = nil
      @user.should_not be_valid
    end

    it 'should be not created without email' do
      @user.email = nil
      @user.should_not be_valid
    end
   
    it 'should be not created without password' do
      @user.password = nil
      @user.should_not be_valid
    end
   
    it 'should be not created if password and password_confirmation not equal' do
      @user.password = 'senha123'
      @user.password_confirmation = 'senha'
      @user.password.should_not == @user.password_confirmation
    end
    
    it 'should be created with all requirements' do
      @user.should be_valid
    end 
  end
 
  O único comportamento diferente é o penúltimo que testa se a senha do usuário está diferente.
 
  Vamos voltar a fazer modificações no Post. Temos agora dois layouts simulando a "home" de um site e "admin", mas nossa rota não indica que "posts" faz parte de um admin, então vá no arquivo routes e modifique o path da rota, e depois é só carregar a página, veja como vai ficar abaixo.
 
  resources :posts, :path => 'admin/posts'
 
  Vamos adicionar autor na nossa tabela posts, já que todo post precisa de um autor, e esse autor será o usuário que estiver armazenado na nossa sessão. Primeiro crie um migração nova que adicionará o campo "author_id" na tabela "posts", será um relacionamento de um autor para muitos posts, e somente um post para um usuário. Crie a migração desta forma:
 
  rails g migration add_field_author_id_to_posts
 
  Abra o arquivo que foi criado e faça as modificações abaixo:
 
  class AddFieldAuthorIdToPosts < ActiveRecord::Migration
    def self.up
      add_column :posts, :author_id, :integer
      add_index :posts, :author_id
    end

    def self.down
      remove_index :posts, :author_id
      remove_column :posts, :author_id
    end
  end
 
  Está adicionando uma nova coluna e um novo índice na tabela posts. Agora abra o model "user" e adicione a linha abaixo, na primeira linha depois da classe ter sido criada.
 
  has_many :posts, :foreign_key => "author_id"
 
  Está dizendo que um usuário terá muitos posts, e sua chave estrangeira é "author_id". Agora no model "post" adicione a linha abaixo, na primeira linha depois da classe ter sido criada.
 
  belongs_to :author, :class_name => "User", :foreign_key => "author_id"
 
  Está dizendo que a chave estrangeira "author_id" pertence a classe "User". Como sempre o post será do usuário logado ao sistema que é um autor, a associação de autor e post não será interferida neste momento. Abra o controller "posts" e faça o código abaixo no método create.
 
  def create
    @post = Post.new params[:post]
    @post.author_id = current_user.id
   
    if @post.save
      flash[:notice] = 'Post was successfully created.'
      respond_with @post
    else
      render :action => :new
    end
  end
 
  Você não viu errado, é só adicionar essa linha mesmo, antes de salvar eu adiciono o id do usuário atual no campo "author_id", esse "current_user" é um método do Devise, retorna um hash com os dados do usuário. Nas views "index" e "show" adicione a saída.
 
  index:
 
  <th>Author</th>
 
  <td><%= post.author.name unless post.author.nil? %></td>
 
  show:
 
  <p>
    <b>Author:</b>
    <%= @post.author.name unless @post.author.nil? %>
  </p>
 
  Para quem não conhece esse "unless" é uma negação, só vai executar se author não for nulo.
 
  Chega por hoje, o próximo post deve ser de Rspec, mas teste de controller.
  Até a próxima!!
 

5 comentários:

  1. oi Vagner,

    Estou testando esse post e aprendendo Rspec.

    Nao seria "rspec spec/models/" em vez de "rake spec:models" ?

    Muito bom seu tutorial.

    []'s
    Sergio Lima
    sergiosouzalima@gmail.com

    ResponderExcluir
  2. Sergio, é "rake spec:models" msm, se vc der no terminal, "rake -T" vc verá as tarefas que o seu sistema pode fazer.

    ResponderExcluir
  3. Eu coloquei um validates_presence_of :author_id no model do Post, como testo isso?

    ResponderExcluir
  4. Saulo, como vc só está validando a presença, no before(:each) coloque o :author_id => 1 e crie assim:
    it 'should be not created without author id' do
    @post.author_id = nil
    @post.should_not be_valid
    end

    e em user_spec vc pode fazer esse teste:
    it 'should have many post' do
    @user.save
    post1 = Post.create(:title => "Title", :body => "Text for body", :publication => Time.now.strftime("%Y-%m-%d %H:%M:%S"), :enabled => 1, :author_id => @user.id)
    post2 = Post.create(:title => "Title 2", :body => "Text for body 2", :publication => Time.now.strftime("%Y-%m-%d %H:%M:%S"), :enabled => 1, :author_id => @user.id)

    @user.posts.should have(2).items
    end

    espero ter ajudado.

    ResponderExcluir