7 coisas que você precisa conhecer no RSpec


Leia em 3 minutos

O RSpec é um framework bastante completo e, por isso mesmo, muitas coisas são desconhecidas por grande parte dos desenvolvedores. Neste artigo, você conhecerá 7 coisas que irão mudar a maneira como você utiliza o RSpec.

Subject

O RSpec possui um método muito útil chamado subject, que retorna uma instância da classe que está sendo utilizada como contexto do exemplo.

describe User do
  it { should_not be_admin }
end

A grande vantagem desta abordagem é que você aumenta consideravelmente a legibilidade de suas especificações. Mas você não precisa utilizar apenas o contexto especificado no método describe; se quiser, você pode definir o seu próprio!

describe User do
  subject { User.new(:admin => true) }
  it { should be_admin }
end

Quando outros tipos de objetos (como módulos ou strings) são passados ao método describe, ele irá retornar o próprio objeto.

Message expectation

O método stub_chain

Sempre que você precisar fazer uma chamada encadeada e quiser verificar o último valor, utilize o método stub_chain. Ele permite transformar um bloco como este

describe "Mocks" do
  before do
    @user = mock()
    @things = mock()
    @recent = mock()

    @user.stub!(:things).and_return(@things)
    @things.stub!(:recent).and_return(@recent)
    @recent.should_receive(:count).and_return(100)
  end

  it "should return the recent things count from an user" do
    @user.things.recent.count.should == 100
  end
end

em uma coisa bem mais simples como esta

describe "Mocks" do
  before do
    @user = mock()
    @user.stub_chain(:things, :recent, :count).and_return(100)
  end

  it "should return the recent things count from an user" do
    @user.things.recent.count.should == 100
  end
end

O método and_return

Você alguma vez precisou acessar um stub mais de uma vez e queria que ele retornasse diferentes valores em cada chamada? Veja este exemplo.

require "open-uri"

class Dice
  API_URL = "http://www.random.org/integers/?num=1&min=1&max=6&col=1&base=10&format=plain&rnd=new"

  def roll
    open(API_URL).to_i
  end
end

Toda vez que o método roll for chamado, ele irá fazer uma requisição diferente ao http://random.org. Imagine que por algum motivo você precisasse retornar diferentes valores toda vez que o método open você invocado. Tudo o que você precisa fazer é retornar mais de um valor com o método and_return.

describe Dice do
  before do
    subject.should_receive(:open).and_return("1", "4", "2")
  end

  it "should not cache request" do
    subject.roll.should == 1
    subject.roll.should == 4
    subject.roll.should == 2
  end
end

O método with

O RSpec disponibiliza uma série de métodos que podem ser usados em conjunto com o método with. Veja alguns exemplos:

module Echo
  extend self

  def echo(*args)
    args.inspect
  end
end

describe Echo do
  # with any kind of argument
  specify("anything") {
    subject.should_receive(:echo).with(anything)
    subject.echo(1)
  }

  # with hash containing values
  specify("hash_including") {
    subject.should_receive(:echo).with(hash_including(:say => "hello"))
    subject.echo(:say => "hello")
  }

  # with an instance of String
  specify("instance_of") {
    subject.should_receive(:echo).with(instance_of(String))
    subject.echo("hello")
  }

  # with an object that responds to some methods
  specify("duck_type") {
    subject.should_receive(:echo).twice.with(duck_type(:<<))
    subject.echo(Array.new)
    subject.echo(String.new)
  }
end

Para ver mais exemplos, acesse https://gist.github.com/45b97f90a83c8acf3f29.

before e after globais

Muitas vezes precisamos preparar nosso ambiente antes de executarmos nossos testes. Quando isso precisa ser feito em mais de uma especificação, você pode diminuir a duplicação de código utilizando blocos globais.

No seu arquivo spec_helper.rb, você pode utilizar os métodos append_before, append_after, prepend_before e prepend_after.

Spec::Runner.configure do |config|
  config.prepend_after { `rm -rf some/path` }
end

Pending

Em vez de comentar temporariamente exemplos que você não quer que sejam executados, você pode utilizar o método pending. O método pending pode ser utilizado nos blocos de before. Assim, todos os exemplos daquele contexto de describe serão automaticamente marcados como pendente.

describe "Pending examples" do
  before do
    pending
  end

  # lots of examples
end

Você também pode deixar apenas um exemplo pendente.

describe "Pending examples" do
  it "should be pending" do
    pending "need to work on it"
    Env.setup!
  end
end

Ou apenas uma parte dele.

describe "Pending examples" do
  it "should be pending with a block" do
    pending("need to working on it as well") do
      Env.setup!
    end
  end
end

Uma outra alternativa (que não é deixar como pendente mas sim desabilitado) é utilizar o método xit em vez de it.

describe "Pending examples" do
  xit "should be pending with alias" do
    Env.setup!
  end
end

Matchers personalizados

Sempre que você perceber que está repetindo um padrão na hora de escrever seus exemplos, automatize o processo criando novos matchers. O RSpec possui duas maneiras diferentes de fazer isso. A mais simples delas é utilizando o método simple_matcher. Crie um módulo chamado TheAnswerMatchers para adicionarmos novos matchers.

module TheAnswerMatchers
  def be_the_answer
    simple_matcher do |given, matcher|
      # save some typing
      the_answer = "the Answer to Life, the Universe, and Everything"

      matcher.description = "be #{the_answer}"
      matcher.failure_message = "expected #{given.inspect} to be #{the_answer}"
      matcher.negative_failure_message = "expected #{given.inspect} not to be #{the_answer}"
      given == 42
    end
  end
end

Agora, basta fazer com que o RSpec reconheça esses novos matchers; é só incluir o módulo.

Spec::Runner.configure do |config|
  config.include TheAnswerMatchers
end

E para escrever seus exemplos, você pode usar o matcher be_the_answer:

describe "The Answer to Life, the Universe, and Everything" do
  specify { 42.should be_the_answer }
  specify { "the wrong answer".should_not be_the_answer }
end

Finalizando…

Como você pode perceber, é bem simples escrever exemplos melhores no RSpec. Se você olhar as specs do RSpec, encontrará bastante coisa legal que você normalmente não veria (porque tem muita coisa para ler) no RDocs.

E você, tem alguma dica para compartilhar?