Iniciando Com Testes de Unidade e Funcionais Usando RSpec

Dando continuidade ao primeiro artigo Iniciando com Testes Ruby Usando RSpec, vamos nos aprofundar um pouco mais neste mundo de teste e entender dois tipos mais comuns, os de Unidade e os Funcionais.

Objetivo

Mostrar como criar testes de unidade e testes funcionais utilizando o RSpec.

Testes de Unidade

São testes pontuais, onde testamos apenas uma porção do código como, por exemplo, um simples método no qual verifica se há uma determinada palavra em um texto. Para exemplificarmos melhor, baixe o projeto do primeiro artigo, https://github.com/wbotelhos/iniciando-com-testes-ruby-usando-rspec, e veja que o teste #language do arquivo article_spec.rb é um teste de unidade, já que testa apenas um pequeno método com uma única lógica de detectar palavras em uma frase:

describe '#language' do
  context 'when title is about Ruby' do
    it 'returns "Ruby"' do
      expect(described_class.language('Aprendendo Ruby')).to eq 'Ruby'
    end
  end
end

Vamos evoluir a nossa aplicação e adicionar alguns campos em nosso modelo com a seguinte migration db/migrate/20140101000001_create_articles.rb:

class CreateArticles < ActiveRecord::Migration
  def up
    create_table :articles do |t|
      t.string   :slug
      t.string   :title
      t.text     :body

      t.timestamps
    end
  end

  def down
    drop_table :articles
  end
end

E dizer que este modelo é persistível via ActiveRecord adicionando a herança:

class Article < ActiveRecord::Base

E então aplicar essa migração:

# Remove as bases de dev, test e prod
rake db:drop:all

# Cria as bases de dev, test e prod
rake db:create:all

# Cria o schema do banco de acordo com as migrations
rake db:migrate

# Aplica as alterações no banco de teste
rake db:test:prepare

Requisitos do cliente

“Ao salvar o artigo, quero que seja criado um slug para ser a URL.”

Para isso iremos usar TDD para pensarmos no resultado antes de pensar em como implementar a melhor lógica. Teremos os seguintes casos:

  • Se o título tiver espaço, o espaço será substituido por hífen;
it 'replaces the space' do
  expect(described_class.slug('a b')).to eq 'a-b'
end
  • Se o título tiver letras maiúsculas, será substituidas por letras minúsculas.
it 'downcases all words' do
  expect(described_class.slug('AB')).to eq ('ab')
end

Ao rodarmos esses testes receberemos o erro undefined method 'slug' for Article:Class, porque ainda não temos o método slug. Então vamos cría-lo:

def self.slug(text)
  text.gsub /\s/, '-'
end

Rodando novamente, fizemos um teste passar, mas ainda não esta deixando tudo em letras minúsculas. Atualizando o método para o a seguir, teremos sucesso:

def self.slug(text)
  text.downcase.gsub /\s/, '-'
end

Baby Steps! Nossos testes de unidades estão escritos e pasando. Estes devem ser o mais simples possível. Podemos dizer que o teste de unidade certifica que o código faz o que o programador quer que ele faça.

Teste Funcional

São diferentes dos de unidade, certificam que o programador esta fazendo o que o cliente deseja. No teste de unidade escrito há pouco, garantimos que o método esta correto, mas não que ao salvar o artigo teremos todo o fluxo e resultado correto.

Vamos criar o controller do artigo com os métodos de salvar e listar:

class ArticlesController < ApplicationController
  def create
    @article = Article.new article_params

    if @article.save
      redirect_to articles_url, notice: 'Artigo salvo com sucesso!'
    else
      flash.notice = 'Erro ao salvar o artigo.'
      render :new
    end
  end

  def index
    @articles = Article.all
  end
end

E as duas páginas usadas por estes métodos:

mkdir -p app/views/articles
touch app/views/articles/index.html.erb
touch app/views/articles/new.html.erb

E por fim as rotas:

IniciandoComTestesDeUnidadeEFuncionaisUsandoRSpec::Application.routes.draw do
  resources :articles
end

Os testes funcionais normalmente ocorrem no controller, pois é lá que especificamos as ações do usuário como, por exemplo, um CRUD.

Listagem de artigos

Para este método temos os seguintes requisitos:

  • Seja executado sem erros;
it 'executes successfully' do
  get :index
  expect(response).to be_successful
end

Para simular o acesso a este método que se chama index usamos o verbo get. Então esperamos que a resposta seja feita com sucesso be_successful.

  • Garantir que este método nos mande para a página correta.
it 'renders the index article page' do
  get :index
  expect(response).to render_template 'articles/index'
end

Como não alteramos o comportamente padrão, a página terá o mesmo nome que o método e estará dentro da pasta com o mesmo nome do controller. Então usamos render_template para dizer que a página index dentro da pasta articles será renderizada.

Criação do artigo

Para este método temos mais requisitos. Pensando no caminho feliz temos:

  • O artigo é salvo com sucesso;
it 'creates a new article' do
  expect {
    post :create, article: { title: 'The Title' }
description: 'The Title' }
  }.to change(Article, :count).by 1
end

Após executarmos o comando de salvar o artigo, experamos que a contagem :count do Article seja alterada change em uma unidade by 1.

  • Somos redirecionados para a listagem de artigos;
it 'redirects to listing page' do
  post :create, article: { title: 'The Title' }
description: 'The Title' }
  expect(response).to redirect_to articles_url
end

A rota articles_url nos manda para o método index, que é a listagem.

  • É enviado uma mensagem de sucesso para a tela.
it 'shows success message' do
  post :create, article: { title: 'The Title' }
description: 'The Title' }
  expect(flash.notice).to eq 'Artigo salvo com sucesso!'
end

A mensagem de sucesso, contida na variável notice tem a descrição correta.

Para fazermos o caminho triste, precisamos ter uma regra para levá-la a falha. Vamos dizer que o título do artigo seja obrigatório:

class Article < ActiveRecord::Base
  validates :title , presence: true

Então para o caminho triste, com o campo title nulo, temos:

  • Não é criado um artigo novo;
it 'does not save the record' do
  expect {
    post :create, article: { title: nil }
description: nil }
  }.to_not change Article, :count
end

Ao salvar o artigo, não há alterações na contagem dos artigos, logo negamos o to para to_not.

  • A página do formulário de criação do artigo é re-renderizada;
it 're-renders the new page' do
  post :create, article: { title: nil }
description: nil }

  expect(response).to render_template 'articles/new'
end
  • A mensagem de erro contida na variável notice é descrita corretamente.
it 'shows flash message' do
  post :create, article: { title: nil }
description: nil }
  expect(flash.notice).to eq 'Erro ao salvar o artigo.'
end

Green! Nosso projeto já tem uns bons testes, agora é manter e dar manutenção nos mesmos. Claro que ainda temos vários casos a cobrir como, por exemplo, verificar se o slug foi gerado corretamente. Mas isso ficará como dever de casa pra vocês.

Assim como o nosso código de produção, os testes são códigos vivos e devem ser mantidos, evoluidos e refatorados. Se você faz TDD ou não, não importa, o importante é testar o que foi criado para que se tenha um feedback rápido e um deploy mais seguro.

Veja esse projeto no Github: https://github.com/wbotelhos/iniciando-com-testes-de-unidade-e-funcionais-usando-rspec