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
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).
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
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
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
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.
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.
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 do
allow(Resource).to receive(:where).with(created_from: params[:id])
.and_return(false)
end
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.
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
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.
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
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.
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.
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".
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
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.
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árias críticas são feitas a estas soluções. Estas soluções são um curativo em um problema que é melhor resolvido com um projeto melhor e com intenções de carregar apenas as dependências que você precisa. Aprenda mais ao ler as discussões relacionadas.
"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.
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
Aprenda mais sobre fuubar.
Livros
<%= render "partials/books" %>Apresentações
Materiais na Web
<%= render "partials/links" %>Screencasts
<%= render "partials/screencasts" %>Bibliotecas (documentação)
<%= render "partials/libraries" %>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:
- Várias línguas (abra uma issue se você quer traduzir este guia)
Créditos
Um obrigado especial ao time Lelylan. Este é um documento licenciado sob a MTI license.
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.