Lenon Marcel meu blog sobre qualquer coisa

RSpec 3 - Verifying doubles

No RSpec 3, especificamente na versão mais recente do projeto rspec-mocks, uma feature muito interessante foi incorporada à ferramenta: a possibilidade de verificar se stub methods estão, de fato, implementados.

Segundo a documentação, os chamados verifying doubles são uma alternativa aos doubles normais. Ao utilizá-los, o RSpec executa verificações que garantem que o método está presente na classe (ou objeto) representado pelo stub, fazendo o teste falhar se um método inválido ou inexistente é chamado. Ele é capaz de checar, inclusive, se o número de argumentos passado é igual ao esperado.

Essa feature é interessante por dois motivos:

  • Ela permite refatorar a interface/protocolo de uma classe sem medo, deixando essa validação no lado do framework de testes.
  • Adiciona um nível muito maior de confiança aos seus testes, com um custo muito baixo.

Para utilizar os verifying doubles, o RSpec provê alguns métodos e configurações que estão detalhados abaixo.

instance_double

Este tipo de verifying double serve para criar stubs de métodos de instância, verificando se estão presentes numa determinada classe. Essa classe é passada como primeiro argumento do double.

Por exemplo, ao criar um stub para um método inexistente...

comment = instance_double('Comment')
expect(comment).to receive(:risos)

... o RSpec falha o teste e exibe a seguinte mensagem:

Comment does not implement: risos

Ao criar um stub com argumentos a menos que os esperados...

comment = instance_double('Comment')
expect(comment).to receive(:risos).with('a')

... o RSpec também falha:

Wrong number of arguments. Expected 2, got 1.

Porém, quando um instance_double é criado para uma classe inexistente, nada acontece. Esse problema é facilmente resolvido através da seguinte configuração do RSpec:

RSpec.configure do |config|
  config.mock_with :rspec do |mocks|
    mocks.verify_doubled_constant_names = true
  end
end

Assim, ao tentar criar um instance_double de uma classe inexistente...

comment = instance_double('Coment')

... o RSpec falha com a seguinte mensagem:

Coment is not a defined constant. Perhaps you misspelt it?

É importante, porém, salientar que o instance_double não sabe verificar métodos implementados através de method_missing. Isso é resolvido por outros tipos de doubles disponíveis.

class_double

Este tipo de verifying double atua da mesma forma que o instance_double, a diferença é que, neste caso, as verificações são contra métodos de classe.

Por exemplo, ao criar um stub para um método de classe inexistente...

comment = class_double('Comment')
expect(comment).to receive(:something)

... o RSpec falha da mesma forma:

Comment does not implement: something

object_double

Este tipo de verifying double pode ser utilizado para criar um double a partir de um objeto existente. Ao contrário do instance_double, um object_double sabe verificar métodos que fazem uso de method_missing.

Por exemplo, ao criar um stub para um método inexistente...

comment = object_double(Comment.new)
#<Comment:0x00000105eb7908> does not implement: something

... o RSpec falha, exatamente como acontece com outros doubles.

Partial doubles

Um partial double é, basicamente, uma extensão de um objeto real. Por exemplo, quando escrevemos o seguinte código...

comment = double('comment')
allow(Comment).to receive(:find) { comment }

... estamos criando um partial double para a classe Comment. Um problema comum com esse tipo de double é que, caso a classe/objeto não implemente o método stubbed, nada acontece (o teste não falha). Até aí nada de novo, já que os partial doubles estão presentes há um bom tempo no RSpec.

Para resolver esse problema, o RSpec 3 introduziu uma configuração chamada de verify_partial_doubles. Quando ativa, as mesmas verificações dos verifying doubles são executadas para os partial doubles.

Para ativar, basta configurar o RSpec da seguinte forma:

RSpec.configure do |config|
  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end
end

Assim, quando um stub é criado para um método inexistente...

comment = double('comment')
allow(Comment).to receive(:blah) { comment }

... o teste falha:

Comment does not implement: blah

Finalizando

Essa feature é uma das melhores novidades introduzidas pelo RSpec 3. Vale investir um tempo atualizando seu projeto, já que o trabalho é pouco e o ganho é enorme. :)

A documentação completa dos verifying doubles pode ser lida aqui.

Cobertura de código Go

Uma das métricas que pode ser utilizada na missão de escrever código com qualidade é a cobertura de código, que fornece informações sobre quais partes de um programa são exercitadas por uma determinada suite de testes.

Para gerar a cobertura de um código escrito em Go, é possível utilizar a ferramenta go test da seguinte forma:

go test -coverprofile=cover.out

Com a flag -coverprofile o comando gera um arquivo de saída com todos os dados de cobertura de código.

Há uma ferramenta disponível no repositório go.tools que recebe este arquivo e gera um HTML com todo o código Go anotado, identificando quais trechos estão sem cobertura.

Para instalar a ferramenta, basta usar go get:

go get code.google.com/p/go.tools/cmd/cover

O arquivo HTML é gerado com o seguinte comando:

go tool cover -html=cover.out -o cover.html

É possível omitir a flag -o, assim o comando abre o HTML gerado diretamente no navegador.

tl;dr

Instale a ferramenta de cobertura através do go get:

go get code.google.com/p/go.tools/cmd/cover

Utilize o seguinte shell script para gerar o arquivo profile e o HTML da cobertura:

cover () {
    profile="$(mktemp -dt $$)/cover.out"
    go test -coverprofile="$profile" $@ && go tool cover -html="$profile"
}

Execute no diretório do seu código:

cover

O HTML da cobertura será aberto no seu navegador.

Algumas dicas de RSpec

A DSL do RSpec é muito rica em recursos. Infelizmente nem sempre conhecemos todos eles. Este post é uma pequena compilação de alguns matchers e sintaxes alternativas não tão conhecidas, mas que podem auxiliar na árdua tarefa de escrever testes concisos e expressivos.

Before com condições

É possível definir condições para um bloco before:

describe Something do
  before(:each, mycondition: true) do
    puts "hey"
  end

  it "should do something", mycondition: true do
    # code...
  end

  it "should do something else" do
    # code...
  end
end

No exemplo acima, o before será executado apenas no primeiro bloco it, pois ele possui o metadado mycondition definido como true.

Expect

Desde o RSpec 2.11 uma nova sintaxe de expectativas (!?) está disponível:

expect(subject.something).to be_true

Apesar do já conhecido should ser mais elegante, ele nem sempre é confiável. Basicamente o RSpec precisa definí-lo em todos os objetos do sistema, o problema é que o RSpec não conhece todos os objetos do sistema. O expect é uma solução para esse tipo de situação.

Você pode ler mais sobre o expect na documentação do RSpec. Para entender exatamente o problema do should, leia este excelente post.

Subjects aninhados (nested subjects)

Assim como em métodos normais do Ruby, é possível utilizar a keyword super para fazer overriding nos blocos subject, let e let!:

describe MyClass do
  subject { MyClass.new("something") }

  describe ".whatever" do
    subject { super().whatever }
  end
end

Uma observação importante é que você deve utilizar os parênteses () ao invocar super. Do contrário, você receberá um erro RuntimeError:

RuntimeError: implicit argument passing of super from method defined by define_method() is not supported. Specify all arguments explicitly.

Described class

Ao descrever uma classe, o RSpec disponibiliza o método described_class, que retorna a classe que a spec descreve. Você pode utilizá-lo, por exemplo, para criar novas instâncias dessa mesma classe:

describe MyClass do
  describe ".whatever" do
    let(:bla) { described_class.new("something") }
  end
end

Isso facilita um futuro refactoring, evitando que você tenha que escrever o nome da classe por todo lado.

Specify

O RSpec possui alguns métodos para descrição de specs. Os mais conhecidos são it, describe e context. Há, além desses, o specify:

describe MyClass do
  subject { described_class.new("Something") }
  specify { subject.title.should eql("Something") }
end

Funcionalmente não há nenhuma diferença entre it e specify. O último é apenas um alias para o primeiro e seu uso deve ser feito com o objetivo de deixar as specs mais expressivas e legíveis.

Its

Outro método útil para deixar as specs mais expressivas é o its. Ele pode ser utilizado ao descrever objetos de retorno:

describe MyApi do
  describe "GET /foo/bar" do
    before { get "/foo/bar" }

    subject { last_response }

    its(:status) { should eql(200) }
    its(:body) { should include "foobarbaz" }
  end
end

Ele suporta, ainda, coisas como nested attributes:

its("phone_numbers.size") { should eq(2) }

E Hashes:

subject do
  { key: "value" }
end
its([:key]) { should eq "value" }

Satisfy

O satisfy é um dos matchers mais flexíveis disponíveis no RSpec. Ele aceita um bloco e espera que o retorno seja true para um teste executado com sucesso:

expect(subject.created_at).to satisfy { |time| (0..5).include? Time.now.to_i - time.to_i }

O único problema é que a mensagem de saída é algo como "expected something to satisfy block", ou seja, não é muito clara.


E você, conhece mais algum truque legal com o RSpec? Compartilhe nos comentários! :)

Usando Resque com Rails

Escrevi um post no blog da Concrete Solutions sobre Resque e como usá-lo com Rails.

http://blog.concretesolutions.com.br/2012/06/resque/

Vale a leitura. :)

Salvando screenshots da tela quando algum teste falha no Capybara/Selenium

Quando você está executando um teste de integração e este teste falha, ver a tela no momento da falha ajuda bastante. Pra isso, você pode usar soluções como o capybara-screenshot ou algo mais simples, sem nenhuma dependência extra:

RSpec.configure do |config|
  config.after(:each) do
    if example.exception
      file = Rails.root.join "tmp/capybara-#{Time.now.to_i}.png"
      Capybara.page.driver.browser.save_screenshot file
    end
  end
end

Note que isto funciona apenas quando o Selenium está configurado como driver.