RSpec é uma ótima ferramenta de desenvolvimento orientado ao comportamento (behavior-driven development – BDD) no processo de escrever especificações que validam diretamente o desenvolvimento da sua aplicação e sejam entendíveis para pessoas.

Na Web existem vários materiais que dão uma visão geral de o _quê_ você pode fazer com RSpec, mas existem poucos materiais com o intuito de mostrar como criar uma boa suíte de testes com RSpec.

Better Specs tenta preencher esta lacuna através da coleta da maioria das "boas práticas" que outros desenvolvedores aprendem em anos de experiência.

Como descrever seus métodos

Seja claro sobre o método que você está descrevendo. Por exemplo, use a convenção da documentação do Ruby de . (ou ::) quando for referir-se ao nome do método de uma classe, e # quando for referir-se ao nome de um método de instância.

ruim

describe 'the authenticate method for User' do
describe 'if the user is an admin' do

bom

describe '.authenticate' do
describe '#admin?' do

Discuta esta boa prática →

Use contexts

Contexts são um poderoso método de tornar seus testes claros e organizados. No longo prazo, esta prática vai manter seus testes legíveis, fáceis de ler.

ruim

it 'has 200 status code if logged in' do
  response.should respond_with 200
end
it 'has 401 status code if not logged in' do
  response.should respond_with 401
end

bom

context 'when logged in' do
  it { is_expected.to respond_with 200 }
end
context 'when logged out' do
  it { is_expected.to respond_with 401 }
end

Ao descrever um contexto, comece sua descrição com "when" (quando) ou "with" (com).

Discuta esta boa prática →

Mantenha a descrição curta

A descrição de um spec nunca deve ter mais do que 40 caracteres. Se isto ocorrer, você deve dividí-lo ao usar context.

ruim

it 'has 422 status code if an unexpected params will be added' do

bom

context 'when not valid' do
  it { should respond_with 422 }
end

No exemplo, nós removemos a descrição relacionado ao código de status, o qual foi substituído pela expectativa it { should respond_with 422 }. Se você rodar este teste ao escrever rspec filename, você obterá uma saída legível.

Saída formatada

when not valid
  it should respond with 422

Discuta esta boa prática →

Testes com expectativa única

A dica de "expectativa única" é comumente expressada como "cada teste deve fazer apenas uma asserção". Isto nos ajuda a encontrar possíveis erros, ao ir diretamente ao teste que falha, e manter o código legível.

Em testes unitários isolados, você quer que cada exemplo especifique um (apenas um) comportamento. Várias expectativas no mesmo exemplo são um sinal de que talvez você esteja especificando vários comportamentos.

Em todo caso, em testes que não são isolados (por exemplo, àqueles integrados ao BD, webservices externos, ou testes end-to-end), você obtém um enorme impacto na performance por fazer o mesmo setup várias vezes apenas para definir expectativas diferentes em cada teste. Neste caso de testes lentos, eu acho aceitável especificar mais de um comportamento isolado.

bom (isolado)

it { should respond_with_content_type(:json) }
it { should assign_to(:resource) }

bom (não isolado)

it 'creates a resource' do
  response.should respond_with_content_type(:json)
  response.should assign_to(:resource)
end
Discuta esta boa prática →

Teste todos os casos possíveis

Testar é uma boa prática, mas se você não testa os casos extremos, isto não será útíl. Teste casos de uso válidos, extremos e inválidos. Por exemplo, considere o action abaixo:

Destroy action

before_filter :find_owned_resources
before_filter :find_resource

def destroy
  render 'show'
  @consumption.destroy
end

O erro que eu comumente vejo encontra-se em testar apenas o caso em que o recurso foi removido. Mas existem, ao menos, dois casos extremos: quando o recurso não é encontrado e quando ele não é proprietário. Como regra de ouro, pense em todas as possíveis entradas e teste-as.

ruim

it 'shows the resource'

bom

describe '#destroy' do

  context 'when resource is found' do
    it 'responds with 200'
    it 'shows the resource'
  end

  context 'when resource is not found' do
    it 'responds with 404'
  end

  context 'when resource is not owned' do
    it 'responds with 404'
  end
end

Discuta esta boa prática →

Use subject

Se você tem vários testes relacionados ao mesmo sujeito, use subject{} para seguir o princípio DRY (Don't repeat yourself – não se repita).

ruim

it { assigns('message').should match /it was born in Belville/ }

bom

subject { assigns('message') }
it { should match /it was born in Billville/ }

RSpec também tem a habilidade de usar um sujeito com nome.

bom

subject(:hero) { Hero.first }
it "carries a sword" do
  hero.equipment.should include "sword"
end

Aprenda mais sobre rspec subject.

Discuta esta boa prática →

Use let e let!

Quando você precisa atribuir uma variável, ao invés de usar um bloco before para criar uma variável de instância, use let. Ao usar let, a variável é carregada apenas quando ela é utilizada pela primeira vez no teste e fica na cache até o teste em questão terminar. Uma boa e profunda descrição sobre o let pode ser encontrada nesta resposta no Stackoverflow.

ruim

describe '#type_id' do
  before { @resource = FactoryGirl.create :device }
  before { @type     = Type.find @resource.type_id }

  it 'sets the type_id field' do
    @resource.type_id.should == @type.id
  end
end

bom

describe '#type_id' do
  let(:resource) { FactoryGirl.create :device }
  let(:type)     { Type.find resource.type_id }

  it 'sets the type_id field' do
    resource.type_id.should == type.id
  end
end

Use let para inicializar ações que são carregadas em modo lazy para testar seus specs.

bom

context 'when updates a not existing property value' do
  let(:properties) { { id: Settings.resource_id, value: 'on'} }

  def update
    resource.properties = properties
  end

  it 'raises a not found error' do
    expect { update }.to raise_error Mongoid::Errors::DocumentNotFound
  end
end

Use let! se você quer definir uma variável quando o bloco é definido. Isto pode ser útil para popular sua base de dados e testar consultas e scopes.

Aqui um exemplo do que realmente é o let.

bom

# isto:
let(:foo) { Foo.new }

# é quase equivalente a isto:
def foo
  @foo ||= Foo.new
end

Saiba mais sobre rspec let.

Discuta esta boa prática →

Utilizar ou não mocks

Existe um debate ocorrendo. Não use (demasiadamente) mocks e teste o comportamente real quando possível. Testar casos reais são úteis ao atualizar o fluxo da sua aplicação.

bom

# simulate a not found resource
context "when not found" do
  before { allow(Resource).to receive(:where).with(created_from: params[:id]).and_return(false) }
  it { should respond_with 404 }
end

Utilizar mocks torna seus specs mais rápidos, mas eles são difíceis de usar. Você precisa entendê-los bem para usá-los bem. Leia mais sobre.

Discuta esta boa prática →

Crie apenas os dados necessários

Se você já trabalhou em um projeto de médio porte (mas também em pequenos), suítes de teste podem ser pesadas para rodar. Para resolver este problema, é importante não carregar mais dados do que o necessário. Além disso, se você acha que precisa de dezenas de dados, provavelmente você está errado.

bom

describe "User"
  describe ".top" do
    before { FactoryGirl.create_list(:user, 3) }
    it { User.top(2).should have(2).item }
  end
end

Discuta esta boa prática →

Use factories, não fixtures

Isto é um tópico antigo, mas bom de relembrar. Não use fixtures porque elas são difíceis de controlar, ao invés disto, use factories. Use-as para reduzir a verbosidade ao criar novos dados.

ruim

user = User.create(
  name: 'Genoveffa',
  surname: 'Piccolina',
  city: 'Billyville',
  birth: '17 Agoust 1982',
  active: true
)

bom

user = FactoryGirl.create :user

Uma nota importante: ao falar sobre teste unitários, a melhor prática deveria ser não usar fixtures ou factories. Coloque o máximo de lógica de domínio em bibliotecas que possam ser testadas sem complexidade e sem consumo de tempo em setup com factories ou fixtures. Leia mais neste artigo.

Aprenda mais sobre Factory Girl.

Discuta esta boa prática →

Matchers fáceis de ler

Use matchers fáceis de ler e cheque duas vezes os rspec matchers disponíveis.

ruim

lambda { model.save! }.should raise_error Mongoid::Errors::DocumentNotFound

bom

expect { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound

Discuta esta boa prática →

Shared Examples

Fazer testes é ótimo e você ficará mais confiante dia após dia, mas ao final você começará a ver duplicação de código vindo de todos os lugares. User shared examples para remover a duplicação da sua suíte de testes (DRY).

ruim

describe 'GET /devices' do
  let!(:resource) { FactoryGirl.create :device, created_from: user.id }
  let(:uri) { '/devices' }

  context 'when shows all resources' do
    let!(:not_owned) { FactoryGirl.create factory }

    it 'shows all owned resources' do
      page.driver.get uri
      page.status_code.should == 200
      contains_owned_resource resource
      does_not_contain_resource not_owned
    end
  end

  describe '?start=:uri' do
    it 'shows the next page' do
      page.driver.get uri, start: resource.uri
      page.status_code.should == 200
      contains_resource resources.first
      page.should_not have_content resource.id.to_s
    end
  end
end

bom

describe 'GET /devices' do

  let!(:resource) { FactoryGirl.create :device, created_from: user.id }
  let(:uri)       { '/devices' }

  it_behaves_like 'a listable resource'
  it_behaves_like 'a paginable resource'
  it_behaves_like 'a searchable resource'
  it_behaves_like 'a filterable list'
end

Na nossa experiência, shared examples são usados principalmente nos controladores. Visto que os modelos são bonitos e diferentes uns dos outros, eles (normalmente) não compartilham muita lógica.

Aprenda mais sobre rspec shared examples.

Discuta esta boa prática →

Teste o que você vê

Teste profundamente seus modelos e o comportamento da sua aplicação (testes de integração). Não adicione complexidade inútil ao testar os controladores.

Quando eu comecei a testar minhas aplicações, eu estava testando os controladores, agora eu não testo. Agora eu apenas crio testes de integração usando RSpec e Capybara. Por quê? Porque eu realmente acredito que você deve testar o que você vê e porque testar controladores é um passo extra desnecessário. Você vai descobrir que a maioria dos seus testes são de modelos e os testes de integração podem ser facilmente agrupados em shared examples, o que criará uma suíte de testes clara e legível.

Isto é um debate em aberto na comunidade Ruby e ambos os lados tem bons argumentos que apóiam as suas ideias. Pessoas que apóiam a necessidade de testar controladores vão dizer que os seus testes de integração não cobrem todos os casos de uso e são lentos.

Ambos estão errados. Você pode facilmente cobrir todos os casos de uso (por que você não deveria?) e você pode executar um único arquivo de teste através de ferramentas de automação como o Guard. Neste caso, você irá executar apenas os specs necessários super rápido sem interromper o seu fluxo.

Discuta esta boa prática →

Não use should

Não use should ao decrever seus testes. Use a terceira pessoa do presente. Melhor ainda, comece a utilizar a nova sintaxe de expectativa.

ruim

it 'should not change timings' do
  consumption.occur_at.should == valid.occur_at
end

bom

it 'does not change timings' do
  expect(consumption.occur_at).to equal(valid.occur_at)
end

Veja a gem should_not como uma forma de reforçar isto no RSpec e a gem should_clean para limpar os exemplos RSpec que começam com "should".

Discuta esta boa prática →

Testes automáticos com guard

Executar toda a suíte de testes cada vez que você altera a aplicação pode ser cansativo. Isto leva muito tempo e pode quebrar o seu fluxo. Com Guard você pode automatizar a execução da sua suíte de testes e executar apenas os testes relacionados ao spec, modelo, controlador ou arquivo atualizado que você está trabalhando.

bom

bundle exec guard
Aqui você pode ver um Guarfile de exemplo com alguma regras de recarregamento.

bom

guard 'rspec', cli: '--drb --format Fuubar --color', version: 2 do
  # run every updated spec file
  watch(%r{^spec/.+_spec\.rb$})
  # run the lib specs when a file in lib/ changes
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
  # run the model specs related to the changed model
  watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
  # run the view specs related to the changed view
  watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
  # run the integration specs related to the changed controller
  watch(%r{^app/controllers/(.+)\.rb}) { |m| "spec/requests/#{m[1]}_spec.rb" }
  # run all integration tests when application controller change
  watch('app/controllers/application_controller.rb') { "spec/requests" }
end

Guard é uma boa ferramenta, mas como de costume, ela não se aplica a todas as suas necessidades. Algumas vezes, seu fluxo TDD funciona melhor com um atalho que facilite a execução de exemplos que você quer, quando você quer. Então, você pode utilizar uma rake task para rodar toda a suíte antes de enviar seu código para o remoto. Atalho do vim aqui.

Aprenda mais sobre guard-rspec.

Discuta esta boa prática →

Testes mais rápidos (pré-carregando o Rails)

Ao rodar testes com Rails, toda a aplicação Rails é carregada. Isto pode levar tempo e quebrar o seu fluxo de desenvolvimento. Para resolver este problema use soluções como Zeus, Spin ou Spork. Estas soluções vão pré-carregar todas as bibliotecas que você (normalmente) não altera e recarregar controladores, modelos, views, factories e todos os arquivos que você altera mais frequentemente.

Aqui você pode achar um spec helper e um Guardfile com configurações baseadas no Spork. Com esta configuração você irá recarregar a aplicação toda se um arquivo pré-carregado (como os initializers) forem alterados e você irá executar os testes muito, muito rápido.

A desvantagem de utilizar o Spork é que ele faz monkey-patches agressivos no seu código e você pode perder algumas horas tentando entender o porquê de um arquivo não ser carregado. Se você tem algum exemplo de código que usa Spin ou qualquer outra solução deixe-nos saber.

Aqui você pode encontrar um Guardfile com configurações para utilizar o Zeus. O spec_helper não precisa ser modificado, entretanto, você ainda precisa executar zeus start em um terminal para iniciar o servidor do zeus antes de executar seus testes.

Entretanto, o Zeus utiliza uma abordagem menos agressiva que o Spork, a maior desvantagem são os requisitos, pois são estritos; Ruby 1.9.3+ (recomenda-se utilizar backported GC from Ruby 2.0), assim como é necessário um sistema operacional que suporte FSEvents ou onitify.

Vários críticas são movidas para estas soluções. Estas soluções são um curativo em um problema que é melhor resolvido com um melhor projeto e com intenções de carregar apenas as dependências que você precisa. Aprenda mais ao ler as discussões relacionadas.

Discuta esta boa prática →

"Fingindo" requisições HTTP

Algumas vezes você precisa acessar serviços externos. Nestes casos você não pode depender de um serviço real, mas você deve "fingir" isto com soluções como webmock.

bom

context "with unauthorized access" do
  let(:uri) { 'http://api.lelylan.com/types' }
  before    { stub_request(:get, uri).to_return(status: 401, body: fixture('401.json')) }
  it "gets a not authorized notification" do
    page.driver.get uri
    page.should have_content 'Access denied'
  end
end

Aprenda mais sobre webmock e VCR. Aqui uma boa apresentação explicando como utilizá-los em conjunto.

Discuta esta boa prática →

Formatadores úteis

Use um formatador que possa dar-lhe informações úteis sobre a suíte de testes. Eu, pessoalmente, acho fuubar muito bom. Para fazê-lo funcionar, adicione a gem e defina o fuubar como o formatador padrão em seu Guardfile.

bom

# Gemfile
group :development, :test do
  gem 'fuubar'

bom

# Guardfile
guard 'rspec' do
  # ...
end

bom

# .rspec
--drb
--format Fuubar
--color

Learn more about fuubar.

Discuta esta boa prática →

Guia de estilo

Nós estamos procurando pelas melhores práticas para escrever specs "bons de ler". Neste momento, um bom ponto de partida é, com certeza, a suíte de testes do Mongoid. Ela usa um estilo limpo e specs fáceis de ler, ao seguir a maioria das boas práticas descritas aqui.

Melhorando Better Specs

Este é um projeto de código aberto. Se tem alguma coisa faltando ou incorreta, apenas reporte uma issue para discutir o tópico. Cheque também as issues à seguir:

Ajude-nos

Se você achou estas dicas úteis e elas melhoraram o seu trabalho, pense sobre fazer uma doação de $9. Qualquer doação será usada para fazer deste site uma referência mais completa para um melhor processo de teste em Ruby.