Skip to content

Instantly share code, notes, and snippets.

@ifasanelli
Last active March 27, 2025 19:46
Show Gist options
  • Save ifasanelli/ab0544386a18aad0741fd3ffbc86af09 to your computer and use it in GitHub Desktop.
Save ifasanelli/ab0544386a18aad0741fd3ffbc86af09 to your computer and use it in GitHub Desktop.
TDD com Ruby on Rails, RSpec e Capybara

0. Índice

Conceitos Iniciais

1. Para que testar software?

  • Garantir a qualidade do software
  • Segurança e facilidade na manutenção
  • Melhor design de software
  • Documentação técnica

2. Tipos de Testes

Principais

  • Unitário
  • Integração
  • Sistema (código legado)

Todos os tipos de Testes (clique aqui)

3. TDD - Test Driven Development ou Desenvolvimento Guiado a Testes

A principal motivação do TDD não é testar o software, e sim especificá-lo com exemplos de como usar o código e deixar isso guiar o design no software.

Fluxo do Teste

  1. Escreva um teste que falhe
  2. Escreva o código para o teste passar
  3. Elimine a redundância

4. Conhecendo o RSpec

Página oficial do RSpec (clique aqui)

Documentação do RSpec 3.9 (clique aqui)

Repositório do RSpec (clique aqui)

Ver Também: Repositório do RSpec-Rails (clique aqui)

Repositório com o código-fonte do projeto (clique aqui)

Instalar a Gem: gem install rspec

Iniciar um projeto no RSpec: rspec --init

Estrutura do RSpec(Em Ruby puro)

  • spec (pasta): Contém os testes
    • _spec (sufixo): Os arquivos de teste devem possuir este sufixo.

Ex.: user_spec.rb

  • lib (pasta): Contém os arquivos do projeto
  • rspec (comando): Roda os testes
  • Gemfile (arquivo): Descreve as gems usadas no projeto
  • RSpec adds ./lib to the $LOAD_PATH: RSpec adiciona ./lib ao $LOAD_PATH

5. Exercitando o TDD com RSpec (Exercício Calculadora)

Entendendo alguns conceitos:

  • describe: descreve o elemento alvo a ser testado.
    Ex.: describe Calculator do; end, onde Calculator é uma classe.

  • it, specify ou example: descrevem o teste.
    Ex.: it 'uses sum method for 2 numbers' do; end

  • expect: método que define o resultado esperado de determinado sujeito.
    Ex.: expect(deck.cards.count).to eq(52) Ex2.: expect(person.happiness).to_not eq('Nil')

Guidelines de boas práticas para RSpec: Better Specs (clique aqui)

Criando o primeiro Teste

Primeiro passo: Criar o Teste

spec/calculator/calculator_spec.rb

require 'calculator'

describe Calculator do
  it 'uses sum metod for 2 numbers' do
    # Arrange
    calc = Calculator.new

    # Act
    result = calc.sum(5, 7)

    # Assert
    expect(result).to eq(11)
  end
  
  it 'uses sum method for 2 negative numbers' do
    # Arrange
    calc = Calculator.new

    # Act
    result = calc.sum(-5, 7)

    # Assert
    expect(result).to eq(2)
  end
end

O teste irá falhar, então começamos, passo a passo, a resolver os erros retornados no console pelo RSpec.

Segundo passo: Criar a classe Calculator

lib/calculator.rb

class Calculator

end

Terceiro passo: Criar o método sum na classe Calculator

lib/calculator.rb

class Calculator
  def sum(a, b)
    a + b
  end
end

Agora o teste irá passar sem nenhum erro.

Para rodar um teste específico utilize o comando: rspec spec/caminho_para_o_teste/meuteste_spec.rb

6. Teste em 4 Fases

  • Testes devem ser confiáveis
  • Testes devem ser fáceis de escrever
  • Testes devem ser fáceis de entender hoje e no futuro
  • Não estamos focados em velocidade!

Um teste do padrão xUnit tem 4 fases, são elas:

  1. Setup ou Arrange: Quando o SUT(System Under Test, o objeto sendo testado) é inserido no estado necessário para que o teste ocorra;

  2. Exercise ou Act: Quando há interação com o SUT;

  3. Verify ou Assert: Quando é verificado o comportamento esperado;

  4. Teardown: Quando o sistema é colocado no estado que estava antes do teste ser executado.
    Ex.: Limpar o banco de dados após o teste ser concluido.
    O Teardown é executado automaticamente pelo RSpec.

7. BDD - Behavior Driven Development ou Desenvolvimento Orientado por Comportamento

O BDD teve sua motivação inicial na dificuldade de Dan North ao explicar TDD para seus alunos quando questionado: Por onde começar a testar? O que testar? Como nomear os testes?
E apesar do BDD ter nascido apenas como um modo de rever a nomenclatura do TDD e o modo como se enxerga essa prática, hoje o BDD é uma abordagem de desenvolvimento de software.

  • A suíte de testes automatizados gerada através do TDD é "apenas" uma boa consequência do processo.
  • Teste -> Comportamento
  • Criou a ferramenta JBehave(Java)

Frameworks mais conhecidas

BDD permite que o cliente participe da especificação dos cenários.

Exemplo utilizando a gem BDD:
context 'Searching' do
  it 'Result is found' do
    Given 'I am on the search page' do
      visit '/search'
      expect(page).to have_content('Search')
    end

    When 'I search something' do
      fill_in 'Search', with: 'John'
      click_button 'Go'
    end

    Then 'I should see the word result' do
      expect(page).to have_content('Result')
    end
  end
end

Saída:

  • Searching
    • Result is found
      • Given I am on the search page
      • When I search something
      • Then I should see the word result

RSpec

Lista com todos os alias do RSpec (clique aqui)

1. Context e .rspec

  • context: cria um contexto onde os testes, que pertencem a ele, estarão alocados.
    • Métodos são descritos em context utilizando as sintaxes:
      • context '.authenticate' do; end, para métodos de classe (def self.authenticate; end)
      • context '#admin?' do; end, para métodos de instância (def admin?; end)

Iremos refatorar o teste da classe Calculator com a finalidade de torná-lo ainda mais organizado e semântico:

require 'calculator'

describe Calculator do
  context '#sum' do
    it 'with positive numbers' do
      calc = Calculator.new
      result = calc.sum(5, 7)
      expect(result).to eq(12)
    end
    
    it 'with negative and positive numbers' do
      calc = Calculator.new
      result = calc.sum(-5, 7)
      expect(result).to eq(2)
    end

    it 'with negative numbers' do
      calc = Calculator.new
      result = calc.sum(-5, -7)
      expect(result).to eq(-12)
    end
  end
end
  • .rspec: reune todas as configuração utilizadas nos testes.

Neste exemplo adicionaremos o formato de documentação em .rspec, para que ao utilizar o comando rspec, essa configuração seja chamada. Não precisando assim ser digitada (rspec --format documentation)

.rspec

--require spec_helper
--format documentation

2. Subject

Subject Implícito

Ao identificarmos a classe no método describe, eliminamos a necessidade de a instanciarmos.
Então podemos utilizar o método subject que faz referência à essa classe.
Veja o exemplo refatorado:

require 'calculator'

describe Calculator do
  context 'use sum method for 2 numbers' do
    it 'with positive numbers' do
      result = subject.sum(5, 7)
      expect(result).to eq(12)
    end

    it 'with negative and positive numbers' do
      result = subject.sum(-5, 7)
      expect(result).to eq(2)
    end

    it 'with negative numbers' do
      result = subject.sum(-5, -7)
      expect(result).to eq(-12)
    end
  end
end

Subject Explícito

Além do subject implícito, que vimos no tópico anterior, temos o subject explícito.
Que nada mais é do que alterar o nome padrão subject para qualquer outro que tenha um significado mais adequado.
Exemplo:

require 'calculator'

describe Calculator do

  subject(:calculator) { described_class.new() }

  context 'use sum method for 2 numbers' do
    it 'with positive numbers' do
      result = calculator.sum(5, 7)
      expect(result).to eq(12)
    end

    it 'with negative and positive numbers' do
      result = calculator.sum(-5, 7)
      expect(result).to eq(2)
    end

    it 'with negative numbers' do
      result = calculator.sum(-5, -7)
      expect(result).to eq(-12)
    end
  end
end

Subject com parâmetros

Caso a classe referenciada no describe necessite de parâmetros, esses devem ser passado via método subject().

Exemplos
Não alterando seu nome:
subject(:subject) { described_class.new(param1, param2, ...) }
Alterando seu nome:
subject(:calculator) { described_class.new(param1, param2, ...) }

Texto complementar no describe

Exemplo:
describe Calculator, 'Sobre a calculadora' do; end

O subject mais interno vence!

Ou seja, quando tivermos describe aninhados, o subject se referirá ao mais interno.
Exemplo:

describe ClassePai do
  describe ClasseFilha do
    subject.metodo == ClasseFilha.metodo
  end
end

One-liner syntax

É uma sintaxe mais enxuta, utiliza a descrição implícita it e, em algums casos, a troca da indicação do subject expect(subject) por is_expected.
Exemplo:

describe (1..5), 'Ranges' do
  # Sintaxe 'normal'
  it '#cover' do
    expect(subject).to cover(2)
  end
  
  # One-liner syntax
  it { is_expected.to cover(2) }
end

3. it, xit e outras coisas

  • it: descreve o teste, mas se ele não tiver um corpo com o teste em si, ele será sinalizado como pendente.

Exemplo:

it 'with negative numbers'
  • xit: se quisermos sinalizar outros teste como pendente, mas que possuem corpo, devemos utilizar o método xit no lugar de it.

Exemplo:

xit 'with negative numbers' do
  result = subject.sum(-5, -7)
  expect(result).to eq(-12)
end

Rodando testes

Sabemos que podemos rodar um único arquivo de teste descrevendo o caminho para esse arquivo.
Exemplo: rspec spec/calculator/calculator_spec.rb

Mas também podemos rodar apenas um teste desse arquivo, utilizando sua descrição ou sua linha.
Exemplo:

calculator_spec.rb

require 'calculator'

describe Calculator do
  context 'use sum method for 2 numbers' do
    it 'with positive numbers' do
      result = subject.sum(5, 7)
      expect(result).to eq(12)
    end

    it 'with negative and positive numbers' do
      result = subject.sum(-5, 7)
      expect(result).to eq(2)
    end

    it 'with negative numbers' do
      result = subject.sum(-5, -7)
      expect(result).to eq(-12)
    end
  end
end

Exemplo utilizando a descrição do teste: rspec spec/calculator/calculator_spec.rb -e 'with negative numbers'

Exemplo utilizando a linha em que o teste começa: rspec spec/calculator/calculator_spec.rb:15

4. Matchers

Matchers são inúmeros comparadores utilizados para a criação de testes mais expressivos.

4.1 Matchers de Igualdade ( equal | be | eql | eq )

  • equal: testa os objetos, logo objetos diferentes com conteúdos igual serão considerados como diferentes.

Exemplo:

describe 'Matchers de Comparação' do
  it '#equal - Testa se o mesmo objeto' do
    x = 'ruby'
    y = 'ruby'
    expect(x).to_not equal(y)
  end
end

O teste passará.

  • be: o método be é um alias para o método equal.

  • eql: testa os valores, invés dos objetos como em equal e be.

Exemplo:

describe 'Matchers de Comparação' do
  it '#eql - Testa os valores' do
    x = 'ruby'
    y = 'ruby'
    expect(x).to eql(y)
  end
end
  • eq: o método eq é um alias para o método eql.

4.2 Matchers de Igualdade ( be true | be_truthy | be false | be_falsey | be_nil )

  • be true: testa se o resultado é verdadeiro.

Exemplo:

it 'be true' do
  expect(1.odd?).to be true
end
  • be_truthy: testa se o resultado é verdadeiro e não nulo.

Exemplo:

it 'be_truthy' do
  expect(1.odd?).to be_truthy
end
  • be false: testa se o resultado é falso.

Exemplo:

it 'be false' do
  expect(1.even?).to be false
end
  • be_falsey: testa se o resultado é falso e não nulo.

Exemplo:

it 'be_falsey' do
  expect(1.even?).to be_falsey
end
  • be_nil: testa se o resultado é nulo.

Exemplo:

it 'be_nil' do
  expect(defined? x).to be_nil
end

4.3 Matchers de Comparação

4.3.1 > | >= | < | <=

  • >: compara se o resultado é maior.

Exemplo:

it '>' do
  expect(5).to be > 1
end
  • >=: compara se o resultado é maior ou igual.

Exemplo:

it '>=' do
  expect(5).to be >= 5
  expect(5).to be >= 2
end
  • <: compara se o resultado é menor.

Exemplo:

it '<' do
  expect(5).to be < 10
end
  • <=: compara se o resultado é menor ou igual.

Exemplo:

it '<=' do
  expect(5).to be <= 5
  expect(5).to be <= 8
end

4.3.2 be_between(min, max).inclusive | be_between(min, max).exclusive | match(/regexp/) | start_with | end_with

  • be_between(min, max).inclusive: compara se o resultado está no range determinado, inclusive.

Exemplo:

it 'be_between' do
  expect(2).to be_between(2,7).inclusive
end
  • be_between(min, max).exclusive: compara se o resultado está no range determinado, exclusive.

Exemplo:

it 'be_between' do
  expect(5).to be_between(2,7).exclusive
end
  • match(/regexp/): compara se o resultado corresponde a expressão regular.

Exemplo:

it 'match' do
  expect('[email protected]').to match(/..@../)
end
  • start_with: compara se o resultado começa com determinada expectativa.

Exemplo:

it 'start_with' do
  expect('fulado de tal').to start_with('fulado')
  expect([1,2,3]).to start_with(1)
end
  • end_with: compara se o resultado termina com determinada expectativa.

Exemplo:

it 'end_with' do
  expect('fulado de tal').to end_with('tal')
  expect([1,2,3]).to end_with(3)
end

4.4 Matchers de Classes/Tipos ( be_instance_of | be_a | be_an | be_kind_of | respond_to )

  • be_instance_of: compara se o resultado é uma instância da classe definida no parâmetro deste método.

Exemplo:

it 'be_instance_of' do
  expect(10).to be_instance_of(Integer)
end
  • be_kind_of: compara se o resultado é do mesmo tipo da classe definida no parâmetro deste método, pertencendo exatamente a mesma classe ou tendo herdado dela.

Exemplo:

class StringNaoVazia < String
  def initialize
    self << 'Não sou vazio'
  end
end
require 'string_nao_vazia'

describe 'Classes' do
  it 'be_kind_of' do
    str = StringNaoVazia.new
    
    expect(str).to be_kind_of(String)
    expect(str).to be_kind_of(StringNaoVazia)
  end
end
  • be_a e be_an: são aliases mais descritivos para o método be_kind_of.

Exemplo:

it 'be_a / be_an' do
  str = StringNaoVazia.new

  expect(str).to be_a(String)
  expect(str).to be_a(StringNaoVazia)
  expect(str).to be_an(String)
  expect(str).to be_an(StringNaoVazia)
end
  • respond_to: verifica se a classe de um determinado objeto responde a um determinado método.

Exemplo:

it 'respond_to' do
  expect('ruby').to respond_to(:size)
  expect('ruby').to respond_to(:count)
end

4.5 Matchers para atributos de Classes ( have_attributes )

  • have_attributes: testa os atributos de classe de determinado objeto.

Exemplo:

class Pessoa
  attr_accessor :nome, :idade
end
require 'pessoa'

describe 'Atributos' do
  it '#have_attributes' do
    pessoa = Pessoa.new
    pessoa.nome = 'Italo'
    pessoa.idade = 33

    expect(pessoa).to have_attributes(nome: 'Italo', idade: 33)
    expect(pessoa).to have_attributes(nome: start_with('I'), idade: (be <= 33))
    expect(pessoa).to have_attributes(nome: starting_with('I'), idade: (be <= 33))
    expect(pessoa).to have_attributes(nome: a_string_starting_with('I'), idade: (a_value <= 33))
  end
end

4.6 Matchers Predicados

Todo método predicado(Clique aqui para saber mais) em Ruby pode ser convertido para um matcher predicado no RSpec, transformando .nil? em be_nil, por exemplo.
Exemplo:

describe 'Predicados' do
  it '#odd' do
    expect(1.odd?).to be true

    # utilizando matcher predicado
    expect(1).to be_odd
  end
end

4.7 Matchers de Erros ( raise_exception | raise_error )

  • raise_exception: um método genérico para testar se a expectativa leventará um erro.

Exemplo:

class Calculator
  def div(a, b)
    a / b
  end
end
require 'calculator'

describe Calculator do
  context '#div' do
    it 'divide by 0' do
      expect{subject.div(3,0)}.to raise_exception
    end
  end
end
  • raise_error: é um método mais específico que o raise_exception pois aceita como parâmetro a classe do erro, a mensagem do erro, as duas ao mesmo tempo ou uma expressão regular.
    É importante que a expectativa seja envolvida entre chaves {} em vez de parênteses () para que o RSpec possa tratá-la como exceção.

Exemplo:

class Calculator
  def div(a, b)
    a / b
  end
end
require 'calculator'

describe Calculator do
  context '#div' do
    it 'divide by 0' do
      expect{subject.div(3,0)}.to raise_error(ZeroDivisionError)
      expect{subject.div(3,0)}.to raise_error('divided by 0')
      expect{subject.div(3,0)}.to raise_error(ZeroDivisionError, 'divided by 0')
      expect{subject.div(3,0)}.to raise_error(/divided/)
    end
  end
end

4.8 Matchers para Arrays ( include | match_array | contain_exactly )

  • include: verifica se o(s) elemento(s), passados como argumento no método, estão incluídos no subject.

Exemplo:

describe Array.new([1,2,3]), 'Array' do
  it '#include' do
    expect(subject).to include(2)
    expect(subject).to include(2,1)
  end
end
  • contain_exactly: verifica se o(s) elemento(s), passados como argumento no método, são exatamente os mesmos do subject, mesmo que em ordem diferente.

Exemplo:

describe Array.new([1,2,3]), 'Array' do
  it '#contain_exactly' do
    expect(subject).to contain_exactly(2,3,1)
  end
end
  • match_array: verifica se o array, passado como argumento no método, é exatamente igual ao subject.

Exemplo:

describe Array.new([1,2,3]), 'Array' do
  it '#match' do
    expect(subject).to match([1,2,3])
  end
end

4.9 Matchers para Ranges

  • cover: verifica se o(s) elemento(s), passados como argumento no método, estão incluídos no subject.

Exemplo:

describe (1..5), 'Ranges' do
  it '#cover' do
    expect(subject).to cover(2)
    expect(subject).to cover(2,5)
    expect(subject).to_not cover(0,6)
  end
end

4.10 Matchers para Coleções

  • all: verifica se o(s) elemento(s), passados como argumento no método, estão incluídos no subject.

Exemplo:

describe 'all' do
  it { expect([1,7,9]).to all( be_odd.and be_an Integer ) }
  it { expect(['ruby', 'rails']).to all( be_a(String).and include('r') ) }
end

4.11 Matcher be_within

Comumente usado para trabalhar com casas decimais, compara o subject com um valor tendo sua diferença, positiva ou negativa, máxima determinada.

  • be_within

Exemplo:

describe 'be_within' do
  it { expect(12.5).to be_within(0.5).of(12) }
  it { expect(11.5).to be_within(0.5).of(12) }
  it { expect(12.2).to be_within(0.5).of(12) }
  it { expect(11.2).to_not be_within(0.5).of(12) }
  it { expect(12.6).to_not be_within(0.5).of(12) }
  it { expect([11.6, 12.1, 12.4]).to all be_within(0.5).of(12) }

  # 11.5 - 11.6 - 11.7 - 11.8 - 11.9 - **12** - 12.1 - 12.2 - 12.3 - 12.4 - 12.5
end

4.12 Matcher satisfy

  • satisfy: verifica se o subject satisfaz determinada expressão.

Exemplo:

describe 'satisfy' do
  it { expect(9).to satisfy{ |num| num % 3 == 0 } }
  
  # Versão mais verbosa
  it {
    expect(9).to satisfy('be a multiple of 3') do |num|
      num % 3 == 0
    end
  }
end

4.13 Matcher change

  • change: detecta quando uma determinada propriedade/método muda de estado.

Exemplo:
lib/contador.rb

class Contador
  @qtd = 0

  def self.qtd
    @qtd
  end

  def self.incrementa
    @qtd += 1
  end
end

spec/matchers/change/cgange_spec.rb

require 'contador'

describe 'Matcher change' do
  # verifica se há alteração do estado da classe
  it { expect{Contador.incrementa}.to change { Contador.qtd } } # qtd == 1

  # verifica se há alteração e se ela ocorre da maneira especificada pelo método by()
  it { expect{Contador.incrementa}.to change { Contador.qtd }.by(1) } # qtd == 2

  # verifica o estado inicial e final da alteração
  it { expect{Contador.incrementa}.to change { Contador.qtd }.from(2).to(3) } # qtd == 3
end

4.14 Matcher output

Métodos que verificam as saídas do console.

  • stdout: standard out, se refere a saída padrão, comumente a tela.

Exemplo:

describe 'Matcher output' do
  it { expect{ puts 'italo' }.to output.to_stdout }
  it { expect{ print 'italo' }.to output('italo').to_stdout }
  it { expect{ puts 'italo' }.to output(/italo/).to_stdout }
end
  • stderr: standard err, verifica se o sistema recebe uma mensagem de erro.

Exemplo:

describe 'Matcher output' do
  it { expect{ warn 'italo' }.to output.to_stderr }
  it { expect{ warn 'italo' }.to output("italo\n").to_stderr }
  it { expect{ warn 'italo' }.to output(/italo/).to_stderr }
end
  • stdin: standard in, se refere a entrada padrão, comumente o teclado.

4.15 Negativando Matchers

Um método que negativa um determinado matcher.
No exemplo a seguir negativaremos o matcher include, chamando esse novo método criado de exclude:

RSpec::Matchers.define_negated_matcher :exclude, :include

describe Array.new([1,2,3]), 'Array' do
  it { expect(subject).to exclude(4) }
  it { expect(subject).to include(2,1) }
end

4.16 Customizando Matchers

A seguir veremos um exemplo de um matcher criado para verificar se um número é múltiplo de outro passado como argumento.
Exemplo:
spec/matchers/custom/custom_soec.rb

RSpec::Matchers.define :be_a_multiple_of do |expected|
  # expected == 3
  # actual == subject == 18
  match do |actual|
    actual % expected == 0
  end

  # mensagem de erro customizada
  failure_message do |actual|
    "expected that #{actual} would be a multiple of #{expected}"
  end

  # mensagem de êxito customizada
  description do
    "be a multiple of #{expected}"
  end
end

describe 18, 'Custom Matcher' do
  it { is_expected.to be_a_multiple_of(3) }
end

Para mais informações sobre matchers customizados, clique aqui!

5. Composição de Expectativas

Pode-se compor as expectativas de duas maneiras, adicionando condições onde todas devem ser cumpridas and ou pelo menos uma delas or.

  • and

Exemplo:

describe 'Ruby on Rails' do
  it { is_expected.to start_with('Ruby').and end_with('Rails') }
end
  • or

Exemplo:

describe 'Composição' do
  it { expect(fruta).to eq('banana').or eq('laranja').or eq('uva') }

  def fruta
    %w(banana laranja uva).sample
  end
end

6. Helper Methods

6.1 Arbitrários e de Módulo

  • arbitrário

Exemplo:

describe 'Arbitrário' do
  it { expect(fruta).to eq('banana').or eq('laranja').or eq('uva') }

  # helper method arbitrário
  def fruta
    %w(banana laranja uva).sample
  end
end
  • de módulo

Exemplo:
spec/helpers/helper.rb

module Helper
  def fruta
    %w(banana laranja uva).sample
  end
end

spec/de_modulo/de_modulo_spec.rb

require_relative '../helpers/helper'

RSpec.configure do |conf|
  # Se utilizado por mais testes, pode ser incluído no spec_helper.rb, não esquecendo de importá-lo.
  conf.include Helper
end

describe 'De módulo' do
  it { expect(fruta).to eq('banana').or eq('laranja').or eq('uva') }
end

6.2 let e let!

  • let: permite a atribuição de uma variável de instância que é carregada apenas quando for utilizada pela primeira vez, ficando armazenada em cache até o término do teste em questão.

Exemplo:

require 'pessoa'

describe 'Atributos' do
  let(:pessoa) { Pessoa.new }

  it '#have_attributes' do
    pessoa.nome = 'Italo'
    pessoa.idade = 33

    expect(pessoa).to have_attributes(nome: start_with('I'), idade: (be<=33))
  end
end
  • let!: força a invocação do método let antes de cada teste.

Exemplo:

$count = 0

describe 'let!' do
  ordem_de_invocacao = []

  let!(:contador) do
    ordem_de_invocacao << :let!
    $count += 1
  end

  it 'chama o método helper antes do teste' do
    ordem_de_invocacao << :exemplo
    expect(ordem_de_invocacao).to eq([:let!, :exemplo])
    expect(contador).to eq(1)
  end
end

7. Hooks

7.1 Before e After

Métodos incluídos no spec_helper.rb ou nos próprios arquivos de teste, que possibilitam a realização de ações antes e depois da execução dos testes.

  • before: realiza ações antes.

Exemplo:
spec/spec_helper.rb

RSpec.configure do |config|
  config.before(:suite) do
    puts '>>> é executado ANTES da execução de TODA a suite de testes'
  end
  
  # pode-se usar o alias :all para o :context
  config.before(:context) do
    puts '>>> é executado ANTES da execução de TODOS os testes dentro do mesmo contexto'
  end
  
  # pode-se usar o alias :each para o :example
  config.before(:example) do
    puts '>>> é executado ANTES da execução de CADA teste'
  end
end
  • after: realiza ações depois.

Exemplo:
spec/spec_helper.rb

RSpec.configure do |config|
  config.after(:suite) do
    puts '>>> é executado DEPOIS da execução de TODA a suite de testes'
  end
  
  # pode-se usar o alias :all para o :context
  config.after(:context) do
    puts '>>> é executado DEPOIS da execução de TODOS os testes dentro do mesmo contexto'
  end
  
  # pode-se usar o alias :each para o :example
  config.after(:example) do
    puts '>>> é executado DEPOIS da execução de CADA teste'
  end
end

Os métodos before() e after() também podem ser inseridos no próprio arquivo de testes.
Exemplo:
spec/matchers/atributos/atributos_spec.rb

describe 'Atributos' do
  before(:example) do
    puts '>>> é executado ANTES de CADA teste'
  end

  after(:example) do
    puts '>>> é executado DEPOIS de CADA teste'
  end

  it '#have_attributes' do
    @pessoa = Pessoa.new
    @pessoa.nome = 'Italo'
    @pessoa.idade = 33

    expect(@pessoa).to have_attributes(nome: 'Italo', idade: 33)
  end
end

7.2 Around

Método que unifica os métodos anteriores before e after em um só, realizando ações antes e depois da execução dos testes.

  • around

Exemplo:
spec/matchers/atributos/atributos_spec.rb

require 'pessoa'

describe 'Atributos' do
  around(:example) do |test|
    # BEFORE
    puts '>>> ANTES de CADA teste'

    test.run

    # AFTER
    puts '>>> DEPOIS de CADA teste'
  end
    
  it '#have_attributes' do
    @pessoa = Pessoa.new
    @pessoa.nome = 'Italo'
    @pessoa.idade = 33

    expect(@pessoa).to have_attributes(nome: 'Italo', idade: 33)
  end
end

8. Agregando Falhas

Tem o objetivo de não interromper o teste após o surgimento da primeira falha, elas então são agregadas e mostradas somente depois que o teste finalizado.

  • aggregate_failures

Exemplo:

describe 'Agregando falhas' do
  it 'be_between / falhas agregadas' do
    aggregate_failures do
      expect(2).to be_between(2,7).inclusive
      expect(1).to be_between(2,7).inclusive
      expect(8).to be_between(2,7).inclusive
    end
  end

  # Ou pode-se inserí-las no it
  it 'be_between / falhas agregadas', :aggregate_failures do
    expect(2).to be_between(2,7).inclusive
    expect(1).to be_between(2,7).inclusive
    expect(8).to be_between(2,7).inclusive
  end
end

Podemos também definir o método aggregated_failures no spec_helper.rb para que valha para toda suite de testes.
Exemplo:
spec_helper.rb

RSpec.configure do |config|
  config.define_derived_metadata do |meta|
    meta[:aggregated_failures] = true
  end
end

9. Shared Examples

Muitas vezes precisamos fazer diversos testes para verificar inúmeros estados de um objeto.
Exemplo:
lib/pessoa.rb

class Pessoa
  attr_accessor :nome, :idade
  attr_reader :status

  def feliz!
    @status = 'Sentindo-se Feliz!'
  end

  def triste!
    @status = 'Sentindo-se Triste!'
  end

  def contente!
    @status = 'Sentindo-se Contente!'
  end
end

spec/shared_examples/not_shared_examples.rb

require 'pessoa'

describe 'Pessoa' do
  subject(:pessoa) { Pessoa.new }

  it 'feliz' do
    pessoa.feliz!
    expect(pessoa.status).to eq('Sentindo-se Feliz!')
  end

  it 'triste' do
    pessoa.triste!
    expect(pessoa.status).to eq('Sentindo-se Triste!')
  end

  it 'contente' do
    pessoa.contente!
    expect(pessoa.status).to eq('Sentindo-se Contente!')
  end
end

Mas desta maneira os testes ficam muito repetitivos, e para resolver isso, utilizamos o método de exemplos compartilhados.
Exemplo:
lib/pessoa.rb

class Pessoa
  attr_accessor :nome, :idade
  attr_reader :status

  def feliz!
    @status = 'Sentindo-se Feliz!'
  end

  def triste!
    @status = 'Sentindo-se Triste!'
  end

  def contente!
    @status = 'Sentindo-se Contente!'
  end
end

spec/shared_examples/shared_examples.rb

require 'pessoa'

describe 'Pessoa' do

  shared_examples 'status' do |sentimento|
    it "#{sentimento}" do
      pessoa.send("#{sentimento}!")
      expect(pessoa.status).to eq("Sentindo-se #{sentimento.capitalize}!")
    end
  end

  subject(:pessoa) { Pessoa.new }

  include_examples 'status', :feliz
  it_behaves_like 'status', :triste
  it_should_behave_like 'status', :contente
end

Lembrando que os métodos it_behaves_like e it_should_behave_like são alias para o método include_examples.
Deve-se utilizar preferivelmente o método it_behaves_like pois o mesmo imprime a ação testada no console deixando o teste mais legível.

Recaptulando o método send

Envia um método para um objeto de forma dinâmica através de texto.

  x = 'italo
  x.size
> 5

  y = 'size'
  x.send(y)
> 5

10. Tag Filters key: true e key: value

São métodos que filtram os testes, com a finalidade de executar uma determinada parte deles.

  • key: true: neste exemplo, a chave terá o nome collection

Exemplo:
spec/tag_filter/key_true_tag_filter_spec.rb

describe 'Tag filter', :collection do
  it { expect([1,7,9]).to all be_odd }
  it { expect(['ruby', 'rails']).to all( be_a(String).and include('r') ) }
end

spec/tag_filter/other_key_true_tag_filter_spec.rb

describe Array.new([1,2,3]), 'Array', collection: true do
  it '#include' do
    expect(subject).to include(2)
    expect(subject).to include(2,1)
  end
end

Obs.: key: true tem o mesmo efeito de :key
Para executar os testes filtrados por essa tag collection, utilizamos os comandos: rspec . -t collection ou rspec . --tag collection

  • key: value: neste exemplo, a chave terá o nome type e o valor collection

Exemplo:
spec/tag_filter/key_value_tag_filter_spec.rb

describe 'Tag filter', type: 'collection' do
  it { expect([1,7,9]).to all be_odd }
  it { expect(['ruby', 'rails']).to all( be_a(String).and include('r') ) }
end

spec/tag_filter/other_key_value_tag_filter_spec.rb

describe Array.new([1,2,3]), 'Array', type: 'collection' do
  it '#include' do
    expect(subject).to include(2)
    expect(subject).to include(2,1)
  end
end

E para executar os testes filtrados por essa tag type: collection, utilizamos os comandos: rspec . -t type:collection ou rspec . --tag type:collection

Para que não precisemos ficar adicionando as flags na hora de chamar os testes, podemos configurá-los no .rspec com a seguinte sintaxe: --tag collection para key:true e --tag type:collection para key:value, dessa maneira as flags serão incluídas automaticamente ao digitarmos o comando rspec

Excluindo testes com flags

Além da possibilidade de marcar testes que gostariamos que fossem realizados em conjunto, podemos também marcar testes que não queremos que rodem utilizando a flag slow em suas descrições.

spec/tag_filter/slow_key_value_tag_filter_spec.rb

describe Array.new([1,2,3]), 'Array', :collection do
  it '#include' do
    expect(subject).to include(2)
    expect(subject).to include(2,1)
  end

  it '#contain_exactly', slow: true do
    expect(subject).to contain_exactly(2,3,1)
  end

  it '#match', :slow do
    expect(subject).to match([1,2,3])
  end
end

Obs.: slow: true tem o mesmo efeito de :slow
Para que a tag tenha efeito, ela deve ser incuída no arquivo .rspec com a seguinte sintaxe: --tag ~slow

11. Test Doubles

Test Doubles ou Dublês de Teste, um termo genérico para qualquer objeto falso, utilizado no lugar de um objeto real, para propósitos de testes.
Em outras palavras, um dublê age como um objeto Ruby, que pode ou não aceitar "mensagens"(métodos).
Dublês são rigorosos/strict, ou seja, você precisa indicar quais mensagens ele aceitará.
Existem vários tipos de dublês de teste existentes, são:

  • mock object
  • stub
  • spy
  • fake object
  • dummy object

O RSpec implementa diretamente três deles, o stub, o mock e o spy.

Exemplo de mock object:
spec/test_doubles/user_spec.rb

describe 'Test Double' do
  it '--' do
    usuario = double('User') # A classe User não existe

    # Maneira menos verbosa
    allow(usuario).to receive_messages(name: 'Italo', password: 'secret')

    # Maneira mais verbosa
    allow(usuario).to receive(:name).and_return('Italo')
    allow(usuario).to receive(:password).and_return('secret')

    usuario.name
    usuario.password
  end
end

11.1 Stubs

Um stub é o ato de forçar uma resposta específica para um determinado método de um objeto colaborador.
São utilizando na fase de Arrange/Setup.
Stubs servem para substituir estados.
Exemplo:
lib/student.rb

class Student
  attr_accessor :name, :email

  def has_finished?(course)
    # retornaria true ou false
  end
end

lib/course.rb

class Course
  attr_accessor :name

  def complete?
    # retornaria true or false
  end
end

spec/stubs/stubs_spec.rb

require 'student'
require 'course'

describe 'Stub' do
  it '#has_finished?' do
    student = Student.new
    course = Course.new

    # stub
    allow(student).to receive(:has_finished?)
                  .with(an_instance_of(Course))
                  .and_return(true)

    course_finished = student.has_finished?(course)

    expect(course_finished).to be_truthy
  end
end

11.1.1 Method Stubs

Stubs com restrição de argumentos
  • receive().with()
  • with(an_instance_of(Class))
Argumentos Dinâmicos

Permite a simulação de diversos argumentos passados para um determinado método.
Exemplo:
lib/student.rb

class Student
  attr_accessor :name, :email

  def foo(args)
    # retornaria alguma coisa
  end
end

spec/stubs/stubs_spec.rb

require 'student'
require 'course'

describe 'Stub' do
  it 'Argumentos Dinâmicos' do
    student = Student.new
    
    # stub
    allow(student).to receive(:foo) do |arg|
      if arg == :hello
        'Olá'
      elsif arg == :hi
        'Hi!!!'
      end
    end

    expect(student.foo(:hello)).to eq('Olá')
    expect(student.foo(:hi)).to eq('Hi!!!')
  end
end
Qualquer instância de uma class

Permite fazer o stub para qualquer instância de uma determinada classe utilizando o método allow_any_instance_of().
Exemplo:
lib/student.rb

class Student
  attr_accessor :name, :email

  def bar
    # retornaria alguma coisa
  end
end

spec/stubs/stubs_spec.rb

require 'student'
require 'course'

describe 'Stub' do
  it 'Qualquer instância de class' do
    student = Student.new
    other_student = Student.new

    # stub
    allow_any_instance_of(Student).to receive(:bar)
                                  .and_return(true)

    expect(student.bar).to be_truthy
    expect(other_student.bar).to be_truthy
  end
end
Testando erros

Permite stubar erros utilizando o método .and_raise(RuntimeError)
Exemplo:
lib/student.rb

class Student
  attr_accessor :name, :email

  def bar
    # retornaria alguma coisa
  end
end

spec/stubs/stubs_spec.rb

require 'student'
require 'course'

describe 'Stub' do
  it 'Erros' do
    student = Student.new

    # stub
    allow(student).to receive(:bar).and_raise(RuntimeError)

    expect{ student.bar }.to raise_error(RuntimeError)
  end
end

11.2 Mocks

São utilizando na fase de Assert/Verify.
Mocks servem para testar comportamentos, com a diferença que a ordem das fases do xUnit nesse caso é alterada de:

  1. Arrange / Setup
  2. Act / Exercise
  3. Assert / Verify

para:

  1. Arrange / Setup
  2. Assert / Verify
  3. Act / Exercise

No caso do exemplo a seguir será verificado se em determinado estudante ocorreu a chamada do método bar

Exemplo:
lib/student.rb

class Student
  attr_accessor :name, :email

  def bar
    # retornaria alguma coisa
  end
end

spec/mocks/mocks_spec.rb

require 'student'

describe 'Mocks' do
  it '#bar' do
    # Arrange / Setup
    student = Student.new

    # Assert / Verify
    expect(student).to receive(:bar)

    #Act / Exercise
    student.bar
  end
end

11.2.1 Mock Expectations

Mock com restrição de argumentos
expect(object).to receive(:message).with(value)

Exemplo:
lib/student.rb

class Student
  attr_accessor :name, :email

  def foo(args)
    # retornaria alguma coisa
  end
end

spec/mocks/mocks_spec.rb

require 'student'

describe 'Mocks' do
  it 'Args' do
    student = Student.new

    expect(student).to receive(:foo).with(123)

    student.foo(123)
  end
end
Mocks com contagem de mensagens(métodos)

Métodos que verificam a quantidade de vezes que um método é invocado.

  • expect(object).to receive(:message).once
  • expect(object).to receive(:message).twice
  • expect(object).to receive(:message).exactly(n).times
  • expect(object).to receive(:message).at_least(:once)
  • expect(object).to receive(:message).at_least(:twice)
  • expect(object).to receive(:message).at_least(n).times

Exemplo:
spec/mocks/mocks_spec.rb

require 'student'

describe 'Mocks' do
  it 'repetição' do
    student = Student.new

    expect(student).to receive(:foo).with(123).twice

    student.foo(123)
    student.foo(123)
  end
end
Mocks com valor de retorno
expect(object).to receive(:message).and_return(value)

Exemplo:
spec/mocks/mocks_spec.rb

require 'student'

describe 'Mocks' do
  it 'retorno' do
    student = Student.new

    expect(student).to receive(:foo).with(123).and_return(true)

    student.foo(123)
  end
end

14. Método "As Null Object"

Método que reconhece como nulo atributos que não foram permitidos allow(obj).to receive(:attr).and_return(value)
Exemplo:
spec/test_doubles/user_spec.rb

describe 'Test Double' do
  it 'as_null_object' do
    usuario = double('User').as_null_object

    allow(usuario).to receive(:name).and_return('Italo')
    allow(usuario).to receive(:password).and_return('secret')

    usuario.name
    usuario.password
    usuario.abcd # atributo não permitido, tem valor nulo
  end
end

15. Configurando RSpec no Rails

No momento da criação da aplicação, utilizamos a flag -T para que não seja configurada a pasta de testes.
Exemplo: rails _version_ new app_name -T

RSpec Rails

Em seguida instalamos a gem do RSpec específica para o Rails, chamada rspec-rails.
Para isso vamos adicioná-la no grupo de desenvolvimento e teste no Gemfile da seguinte maneira:

group :development, :test do
  gem 'rspec-rails', '~> 3.9'
end

E para efetivarmos essa instalação rodamos o comando bundle install

Caso a aplicação já esteja configurada e o RSpec for adicionado após isso, deve se verificar a presença de um banco de dados dedicado a testes em config/database.yml
Exemplo:

test:
  <<: *default
  database: db/test.sqlite3

Rodar o comando rails db:create:all para a criação dos bancos de dados de desenvolvimento, teste e produção.

E para gerar os arquivos de configuração padrão do RSpec rodamos o comando rails generate rspec:install
Os arquivos de configuração padrão do RSpec são: .rspec, spec, spec/spec_helper.rb, spec/rails_helper.rb

Lembrando que para habilitarmos a opção de formato de documentação precisamos inserir --format documentation em .rspec

O RSpec por padrão criará arquivos para teste de tudo que criarmos na aplicação e para limitarmos essas ações devemos configurar o config/applicarion.rb da seguinte maneira:

class Application < Rails::Application
    config.load_defaults 5.2

    # Don't generate system test files.
    config.generators.system_tests = nil

    config.generators do |g|
      g.test_framework :rspec,
       fixtures: false,
       view_specs: false,
       helper_specs: false,
       routing_specs: false,
       request_specs: false
    end
  end

RSpec binstub

Para ganharmos mais velocidade na execução dos testes, utilizaremos a gem spring-commands-rspec que criará um binário que executará os comandos do RSpec diretamente em vez de serem executadas pelo bundle exec.
Para isso adicionamos a gem no grupo de desenvolvimento no Gemfile.
Exemplo:

group :development do
  gem 'spring-commands-rspec'
end

E para efetivarmos a instalação do spring-commands-rspec rodamos o comando bundle install

Para gerarmos os binários desejados, temos dois comandos:

  • bundle exec spring binstub rspec: para gerar o binário do RSpec
  • bundle exec spring binstub --all: para gerar o binário de tudo possivel, caso tenham gem configuradas para tal

E finalmente para que possamos executar o RSpec pelo binário recém criado utilizaremos o comando bin/rspec de agora em diante.

Capybara

Capybara é uma gem essencial para que os testes nas views sejam realizados, simulando como um usuário real interagiria com a aplicação.
E para instalarmos o capybara devemos adicioná-la no grupo de desenvolvimento e teste no Gemfile da seguinte maneira:

group :development, :test do
  gem 'capybara'
end

Para efetivarmos a instalação do capybara rodamos o comando bundle install

E por fim, configurá-la no spec_helper.rb dando um require no topo do arquivo require 'capybara/rspec'

16 Testando Models

Documentação

Primeiramente precisaremos gerar dois models para que possamos testá-los:

  • Categoria: rails g model category description
  • Produto: rails g model product description price:decimal category:references

Caso tenhamos instalado o RSpec depois que os models tenham sido gerados, precisamos executar um comando para que os arquivos de teste sejam gerados: rails g rspec:model <nome-do-model>

16.1 Specs Básicas

  • Quando instanciado com atributos válidos, o model deve ser válido.
  • Validações devem ser testadas.
  • Métodos de classe e intância devem executar corretamente.

16.2 Specs - Model Produto

models/product.rb

class Product < ApplicationRecord
 belongs_to :category
 validates :description, :price, :description, presence: true
 
 def full_description
   "#{description} - #{price}"
 end
end

16.2.1 Quando instanciado com atributos válidos, o model deve ser válido.

  • It 'is valid with description, price and category'
    spec/models/product_spec.rb
require 'rails_helper'

RSpec.describe Product, type: :model do
  it 'is valid with description, price and category' do
    produto = create(:product)
    expect(produto).to be_valid
  end
end

16.2.2 Validações devem ser testadas.

  • Descrição inválida
    spec/models/product_spec.rb
require 'rails_helper'

RSpec.describe Product, type: :model do
  it 'is invalid without description' do
    produto = build(:product, description: nil)
    produto.valid?
    expect(produto.errors[:description]).to include("can't be blank")
  end
end
  • Preço inválido
    spec/models/product_spec.rb
require 'rails_helper'

RSpec.describe Product, type: :model do
  it 'is invalid without price' do
    produto = build(:product, price: nil)
    produto.valid?
    expect(produto.errors[:price]).to include("can't be blank")
  end
end
  • Categoria inválida
    spec/models/product_spec.rb
require 'rails_helper'

RSpec.describe Product, type: :model do
  it 'is invalid without category' do
    produto = build(:product, category: nil)
    produto.valid?
    expect(produto.errors[:category]).to include('must exist')
  end
end

16.2.3 Métodos de classe e intância devem executar corretamente.

  • Método full_description
    spec/models/product_spec.rb
require 'rails_helper'

RSpec.describe Product, type: :model do
  it '#full_description' do
    produto = create(:product)
    expect(produto.full_description).to eq("#{produto.description} - #{produto.price}")
  end
end

FactoryBot e VCR

1. Fixtures

São dados falsos que deixamos cadastrados para facilitar na hora dos testes, assim não precisamos preencher os atributos dos elementos que usaremos.
O RSpec configura as fixtures para uma pasta dedicada no diretório spec em rails_helper.rb.
config.fixture_path = "#{::Rails.root}/spec/fixtures"
Então criaremos essa pasta e consecutivamente uma fixture para trabalharmos com o model Customer, e o arquivo ficará assim:
Exemplo:
spec/fixtures/customers.yml

italo:
  name: Italo Fasanelli
  email: [email protected]

mariana:
  name: Mariana Santana
  email: [email protected]

Então criaremos nosso teste que verificará o retorno de um método criado no model.
Exemplo:
app/models/customer.rb

class Customer < ApplicationRecord
  def full_name
    "Sr(a). #{name}"
  end
end

spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  fixtures :customers

  it 'Create a Customer' do
    customer = customers(:italo)

    expect(customer.full_name).to eq('Sr(a). Italo Fasanelli')
  end
end

Quando temos um número considerável de fixtures, podemos alterar a chamada da fixture de fixtures :customers para fixtures :all, assim todas as fixtures serão importadas para o teste.

2. FactoryBot Rails

Repositório FactoryBotRails

Uma poderosa ferramenta para criarmos e utilizarmos fixtures.

2.1 Instalação

Gemfile

group :development, :test do
  gem 'factory_bot_rails'
end

Para efetivar a instalação rode o comando bundle install

2.2 Configurando

Permite o Rails aceitar os comandos do FactoryBot
spec/rails_helper.rb

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

2.3 Definindo Factories

Primeiramente criaremos o diretório spec/factories para que possamos armazená-las.
Em seguida, vamos criar nossa primeira factory do model Customer, que ficará assim:
spec/factories/customer.rb

FactoryBot.define do
  factory :customer do
    name 'Italo Fasanelli'
    email '[email protected]'
  end
end

2.4 Utilizando Factories

  • build(:factory_name): cria uma instância do objeto da factory mas não a salva no banco de dados do teste.

Exemplo:
spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it 'Create a Customer' do
    customer = build(:customer)

    expect(customer.full_name).to eq('Sr(a). Italo Fasanelli')
  end
end
  • create(:factory_name): cria uma instância do objeto da factory e a salva no banco de dados do teste.

Exemplo:
spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it 'Create a Customer' do
    customer = create(:customer)

    expect(customer.full_name).to eq('Sr(a). Italo Fasanelli')
  end
end
  • attributes_for: método que traz apenas os atributos, em um hash, do elemento passado como argumento.
    Muito utilizado para testes de API, para testar JSON.
    Exemplo:
    spec/models/customer_spec.rb
require 'rails_helper'

RSpec.describe Customer, type: :model do
  it 'Usando o attributes_for' do
    attrs = attributes_for(:customer)
    attrs1 = attributes_for(:customer_vip)
    puts attrs
    puts attrs1
  end
end

# Retorno:
{:name=>"Carl Medhurst", :email=>"[email protected]", :vip=>false, :days_to_pay=>15}
{:name=>"Saundra Hodkiewicz", :email=>"[email protected]", :vip=>true, :days_to_pay=>30}

Exemplo 2:
spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it 'Usando o attributes_for 2' do
    attrs = attributes_for(:customer)
    customer = Customer.create(attrs)

    expect(customer.full_name).to start_with('Sr(a). ')
  end
end

2.5 Herança

Herdará todos os atributos não definidos nas factories customizadas.
Exemplo:
spec/factories/customer.rb

FactoryBot.define do
  factory :customer, aliases: [:user, :buyer] do
    name { Faker::Name.name }
    email { Faker::Internet.email }
    vip { false }
    days_to_pay { 15 }

    factory :custumer_vip do
      vip { true }
      days_to_pay { 30 }
    end
  end
end

spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it 'Herança' do
    customer = create(:custumer_vip)
    expect(customer.vip).to be true
  end
end

2.6 Atributos Transitórios (Transient Attributes)

São atributos que, apesar de declarados na factory, não irão existir no teste.

Exemplo:
spec/factories/customer.rb

FactoryBot.define do
  factory :customer, aliases: [:user, :buyer] do

    transient do
      upcased { false }
    end

    name { Faker::Name.name }
    email { Faker::Internet.email }
    vip { false }
    days_to_pay { 15 }

    after(:create) do |customer, evaluator|
      customer.name.upcase! if evaluator.upcased
    end
  end
end

spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it 'Atributo Transitório' do
    customer = create(:customer, upcased: true)
    expect(customer.name.upcase).to eq(customer.name)
  end
end

2.7 Sobrescrevendo Atributos e Aliases para Factories

2.7.1 Sobrescrevendo Atributos

Exemplo:
spec/factories/customer.rb

FactoryBot.define do
  factory :customer, aliases: [:user, :buyer] do
    name { Faker::Name.name }
    email { Faker::Internet.email }
  end
end

spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it '#full_name' do
    customer = create(:customer, name: 'Italo Fasanelli')

    expect(customer.full_name).to eq('Sr(a). Italo Fasanelli')
  end

  it { expect{ create(:customer) }.to change{Customer.all.size}.by(1) }
end

2.7.2 Aliases para Factories

Exemplo:
spec/factories/customer.rb

FactoryBot.define do
  factory :customer, aliases: [:user, :buyer] do
    name { Faker::Name.name }
    email { Faker::Internet.email }
  end
end

Exemplo:
spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it '#full_name' do
    customer = create(:buyer)
    expect(customer.full_name).to start_with('Sr(a). ')
  end

  it { expect{ create(:user) }.to change{Customer.all.size}.by(1) }
end

2.8 Traits

Tem a finalidade de agrupar atributos e com eles criar novas factories.
Exemplo:
spec/factories/customer.rb

FactoryBot.define do
  factory :customer, aliases: [:user, :buyer] do

    name { Faker::Name.name }
    email { Faker::Internet.email }
    vip { false }
    days_to_pay { 15 }

    trait :male do
      gender { 'M' }
    end

    trait :female do
      gender { 'F' }
    end

    trait :vip do
      vip { true }
      days_to_pay { 30 }
    end

    factory :customer_male, traits: [:male]
    factory :customer_female, traits: [:female]
    factory :customer_vip, traits: [:vip]
    factory :customer_male_vip, traits: [:vip, :male]
    factory :customer_female_vip, traits: [:vip, :female]
  end
end

spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it 'Cliente Masculino' do
    customer = create(:customer_male)
    expect(customer.gender).to eq('M')
    expect(customer.vip).to be false
  end

  it 'Cliente Masculino VIP' do
    customer = create(:customer_male_vip)
    expect(customer.gender).to eq('M')
    expect(customer.vip).to be true
  end
end

2.9 Callbacks

  • after(:build): executado depois de ser criado em memória com :build ou :create
  • before(:create): executado antes de efetivamente salvar no banco de dados
  • after(:create): executado depois de efetivamente salvar no banco de dados

spec/factories/customer.rb

FactoryBot.define do
  factory :customer, aliases: [:user, :buyer] do
    after(:create) do |customer, evaluator|
      customer.name.upcase! if evaluator.upcased
    end
  end
end

2.10 Sequences

Método utilizando para criar sequencias numéricas nos testes.
Exemplo:
spec/factories/customer.rb

FactoryBot.define do
  factory :customer, aliases: [:user, :buyer] do

    name { Faker::Name.name }
    vip { false }
    days_to_pay { 15 }
    #email { Faker::Internet.email }
    sequence(:email) { |n| "email-num-#{n}@email.com" }
    
  end
end

spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it 'Sequence' do
    customer1 = create(:customer)
    customer2 = create(:customer)
    puts customer1.email
    puts customer2.email
  end
end

# Retorno:
email-num-11@email.com
email-num-12@email.com

Exemplo com n(número) inicial:
spec/factories/customer.rb

FactoryBot.define do
  factory :customer, aliases: [:user, :buyer] do

    name { Faker::Name.name }
    vip { false }
    days_to_pay { 15 }
    #email { Faker::Internet.email }
    sequence(:email, 35) { |n| "email-num-#{n}@email.com" }
    
  end
end

spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it 'Sequence' do
    customer1 = create(:customer)
    customer2 = create(:customer)
    puts customer1.email
    puts customer2.email
  end
end

# Retorno:
email-num-35@email.com
email-num-36@email.com

Exemplo com l(letra) inicial:
spec/factories/customer.rb

FactoryBot.define do
  factory :customer, aliases: [:user, :buyer] do

    name { Faker::Name.name }
    vip { false }
    days_to_pay { 15 }
    #email { Faker::Internet.email }
    sequence(:email, 'a') { |l| "email-num-#{l}@email.com" }
    
  end
end

spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it 'Sequence' do
    customer1 = create(:customer)
    customer2 = create(:customer)
    puts customer1.email
    puts customer2.email
  end
end

# Retorno:
email-num-a@email.com
email-num-b@email.com

2.11 Associação: belongs_to

Utiliza a herança da mesma maneira que o Rails.
Exemplo: app/models/order.rb

class Order < ApplicationRecord
  belongs_to :customer
end

spec/factories/orders.rb

FactoryBot.define do
  factory :order do
    sequence(:description) { |n| "Pedido número #{n}" }
    customer
  end
end

spec/models/order_spec.rb

require 'rails_helper'

RSpec.describe Order, type: :model do
  it 'Tem 1 pedido' do
    order = create(:order)
    expect(order.customer).to be_kind_of(Customer)
  end
end

2.11.1 Sobrescrevendo atributos

Exemplo: app/models/order.rb

class Order < ApplicationRecord
  belongs_to :customer
end

spec/factories/orders.rb

FactoryBot.define do
  factory :order do
    sequence(:description) { |n| "Pedido número #{n}" }
    customer
  end
end

spec/models/order_spec.rb

require 'rails_helper'

RSpec.describe Order, type: :model do
  it 'Sobrecrevendo atributos' do
    customer = create(:customer, name: 'João Gilberto')
    order = create(:order, customer: customer)
    expect(order.customer.name).to eq('João Gilberto')
  end
end

2.11.2 Sobrescrevendo factories

Exemplo: app/models/order.rb

class Order < ApplicationRecord
  belongs_to :customer
end

spec/factories/orders.rb

FactoryBot.define do
  factory :order do
    sequence(:description) { |n| "Pedido número #{n}" }
    association :customer, factory: :customer
  end
end

spec/models/order_spec.rb

require 'rails_helper'

RSpec.describe Order, type: :model do
  it 'Tem 1 pedido' do
    order = create(:order)
    expect(order.customer).to be_kind_of(Customer)
  end
end

2.12 Create List

Tem a finalidade de facilitar a criação do teste quando precisamos criar vários objetos.
create_list(:factory, qt_objetos)
Exemplo:
spec/models/order_spec.rb

require 'rails_helper'

RSpec.describe Order, type: :model do
  it 'Tem 3 pedidos' do
    orders = create_list(:order, 3)
    expect(orders.count).to eq(3)
  end
end

2.13 Associação: has_many

Utiliza a herança da mesma maneira que o Rails.
Exemplo:

app/models/customer.rb

class Customer < ApplicationRecord
  has_many :orders
end

spec/factories/customers.rb

FactoryBot.define do
  factory :customer do

    transient do
      qt_orders { 3 }
    end

    trait :with_orders do
      after(:create) do |customer, evaluator|
        create_list(:order, evaluator.qt_orders, customer: customer)
      end
    end
    
    #Criando uma factory usando uma trait
    factory :customer_with_orders, traits: [:with_orders]
    
  end
end

Exemplo:
spec/models/order_spec.rb

require 'rails_helper'

RSpec.describe Order, type: :model do
  it 'Has_many' do
    customer = create(:customer, :with_orders)
    expect(customer.orders.count).to eq(3)
    
    # Uma factory usando trait
    customer = create(:customer_with_orders)
    expect(customer.orders.count).to eq(3)

    #Sobrescrevendo a quantidade de orders, definimos 3 na factory
    customer = create(:customer, :with_orders, qt_orders: 5)
    expect(customer.orders.count).to eq(5)
  end
end

2.14 Métodos extras

  • create_list(:factory, qt_elements): Cria uma lista de elementos no banco de dados de teste.
  • build_list(:factory, qt_elements): Cria uma lista de elementos em memória.
  • create_pair(:factory): Cria um par de elementos no banco de dados de teste.
  • build_pair(:factory): Cria um par de elementos em memória.
  • attributes_for(:factory): Extrai apenas os atributos de determinado objeto.
  • attributes_for_list(:factory): Extrai apenas os atributos de determinada lista de objetos.
  • build_sttubed(:factory): Cria um objeto falso(mockado, sttubado) a partir de uma factory.
  • build_sttubed_list(:factory): Cria uma lista de objetos falsos a partir de uma factory.

2.15 FactoryBot Lint

É uma ação tomada pelo FactoryBot em verificar as validações dos modelse outras configurações e reporta os erros do teste de maneira mais organizada.
Lembrando que essa ação exigirá mais do processamento dos testes.
Para saber mais sobre o FactoryBot Lint, clique aqui
spec/spec_helper.rb

config.before(:suite) do
  FactoryBot.lint
end

3. Gem Faker

Repositório Faker

Uma ferramenta que gera dados falsos, que auxiliará na utilização das factories.

3.1 Instalação

Gemfile

group :development, :test do
  gem 'faker'
end

Para efetivar a instalação rode o comando bundle install

3.2 Implementação

Exemplo:
spec/factories/customer.rb

FactoryBot.define do
  factory :customer do
    name { Faker::Name.name }
    email { Faker::Internet.email }
  end
end

Exemplo:
spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it '#full_name' do
    customer = create(:customer)

    expect(customer.full_name).to start_with('Sr(a). ')
  end

  it { expect{ create(:customer) }.to change{Customer.all.size}.by(1) }
end

4. Gem HTTParty

Repositório HTTParty

Permite acesso à internet e seus serviços.

4.1 Instalação

Gemfile

group :development, :test do
  gem 'httparty'
end

Ou pelo comando: gem install httparty

4.2 Exemplos

4.2.1 Na aplicação Rails

spec/httparty/httparty_spec.rb

require 'rails_helper'

describe 'HTTParty' do
  xit 'HTTParty' do
    response = HTTParty.get('https://jsonplaceholder.typicode.com/users/2')
    content_type = response.headers['content-type']

    expect(content_type).to match(/json/)
  end
end

5. Gem Webmock

Repositório Webmock
Invés de efetivamente acessarmos a internet como no tópico anterior, simularemos(stub) esse acesso com a finalidade de facilitar a execução do teste.

5.1 Instalação

Gemfile

group :development, :test do
  gem 'webmock'
end

E adicionamos require 'webmock/rspec' na primeira linha do arquivo spec/spec_helper.rb

5.2 Exemplo

spec/httparty/httparty_spec.rb

require 'rails_helper'

describe 'HTTParty' do
  it 'Content type - stub' do
    stub_request(:get, "https://jsonplaceholder.typicode.com/users/2").
      to_return(status: 200, body: '', headers: {'content-type': 'application/json'})

    response = HTTParty.get('https://jsonplaceholder.typicode.com/users/2')
    content_type = response.headers['content-type']

    expect(content_type).to match(/json/)
  end
end

6. VCR

Repositório VCR
Grava as interações HTTP do conjunto de testes e as reproduz durante futuras execuções de teste para aumentar a velocidade e a precisão dos testes.

6.1 Instalação

Gemfile

group :development, :test do
  gem 'vcr'
end

spec/spec_helper.rb (no início do arquivo)

VCR.configure do |config|
  config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
  config.hook_into :webmock
end

A configuração acima define o diretório spec/fixtures/vcr_cassettes como diretório onde serão criadas as cassetes.
Ao executarmos o teste pela primeira vez, ele irá armazenar neste diretório toda a resposta da requisição em um arquivo .yml e a partir da segunda vez que executarmos o teste, ele usará esse cassete.

6.2 Exemplo

spec/httparty/httparty_spec.rb

require 'rails_helper'

describe 'HTTParty' do
  it 'Content type - vcr' do
    VCR.use_cassette('jsonplaceholder/posts') do
      response = HTTParty.get('https://jsonplaceholder.typicode.com/users/2')
      content_type = response.headers['content-type']
      expect(content_type).to match(/json/)
    end
  end
end

spec/fixtures/vcr_cassettes/jsonplaceholder/posts.yml

---
  http_interactions:
  - request:
      method: get
      uri: "https://jsonplaceholder.typicode.com/users/2"
      body:
        encoding: US-ASCII
        string: ''
      headers:
        Accept-Encoding:
        - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
        Accept:
        - "*/*"
        User-Agent:
        - Ruby
    response:
      status:
        code: 200
        message: OK
      headers:
        Date:
        - Sun, 19 Nov 2017 19:17:08 GMT
        Content-Type:
        - application/json; charset=utf-8
        Transfer-Encoding:
        - chunked
        Connection:
        - keep-alive
        Set-Cookie:
        - __cfduid=deaab0068441ff161587338e66121456a1511119028; expires=Mon, 19-Nov-18
          19:17:08 GMT; path=/; domain=.typicode.com; HttpOnly
        X-Powered-By:
        - Express
        Vary:
        - Accept-Encoding
        Access-Control-Allow-Credentials:
        - 'true'
        Cache-Control:
        - public, max-age=14400
        Pragma:
        - no-cache
        Expires:
        - Sun, 19 Nov 2017 23:17:08 GMT
        X-Content-Type-Options:
        - nosniff
        Etag:
        - W/"116-jnDuMpjju89+9j7e0BqkdFsVRjs"
        Via:
        - 1.1 vegur
        Cf-Cache-Status:
        - HIT
        Server:
        - cloudflare-nginx
        Cf-Ray:
        - 3c058205ef884aeb-GRU
      body:
        encoding: ASCII-8BIT
        string: |-
          {
            "userId": 1,
            "id": 2,
            "title": "qui est esse",
            "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
          }
      http_version:
    recorded_at: Sun, 19 Nov 2017 19:17:04 GMT

6.3 VCR com metadados do RSpec

Documentação
Remove a necessidade de especificarmos o diretório e o nome da cassete nos testes, deixando essa função para o VCR.

6.3.1 Configurando

Adicionamos config.configure_rspec_metadata! ao bloco de configuração do VCR.
Exemplo:
spec/spec_helper.rb (no início do arquivo)

VCR.configure do |config|
  config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
  config.hook_into :webmock
  config.configure_rspec_metadata!
end

6.3.2 Exemplos

spec/httparty/httparty_spec.rb

require 'rails_helper'

describe 'HTTParty' do
  it 'content-type-vcr-metadata', :vcr do
    response = HTTParty.get('https://jsonplaceholder.typicode.com/users/2')
    content_type = response.headers['content-type']
    expect(content_type).to match(/json/)
  end
end

spec/fixtures/vcr_cassettes/HTTParty/content-type-vcr-metadata.yml

---
  http_interactions:
  - request:
      method: get
      uri: "https://jsonplaceholder.typicode.com/users/2"
      body:
        encoding: US-ASCII
        string: ''
      headers:
...

E para utilizarmos um cassete específico em um ou mais testes, definimos ele dessa maneira:
spec/httparty/httparty_spec.rb

require 'rails_helper'

describe 'HTTParty' do
  it 'content-type - vcr metadata cassete_name', vcr: { cassette_name: 'jsonplaceholder/posts' } do
    response = HTTParty.get('https://jsonplaceholder.typicode.com/users/2')
    content_type = response.headers['content-type']
    expect(content_type).to match(/json/)
  end
end

6.4 Filtrando dados sensíveis

Documentação
Permite configurarmos que dados específicos não sejam gravados na cassete.

6.4.1 Configurando

Adicionamos config.configure_rspec_metadata ao bloco de configuração do VCR.
Exemplo:
spec/spec_helper.rb (no início do arquivo)

VCR.configure do |config|
  config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
  config.hook_into :webmock
  config.configure_rspec_metadata!
  config.filter_sensitive_data('<URL-API>') { 'https://jsonplaceholder.typicode.com' }
end

No caso acima, estamos configurando que para todas as ocorrências da URL do jsonplaceholder, a tag <URL-API> aparecerá em seu lugar.
Resultando no yml: spec/fixtures/vcr_cassettes/HTTParty/content-type-vcr-metadata.yml

---
  http_interactions:
  - request:
      method: get
      uri: "<URL-API>/users/2"
      body:
        encoding: US-ASCII
        string: ''
      headers:
...

6.5 VCR com URIs não determinísticas

Documentação
Essa configuração permite que VCR utilize o body da resposta da requisição e não mais o verbo HTTP e URL para verificar a expectativa.

6.5.1 Exemplo

spec/httparty/httparty_spec.rb

require 'rails_helper'

describe 'HTTParty' do
  it 'vcr-matching-body', vcr: { match_requests_on: [:body], cassette_name: 'jsonplaceholder/posts' } do
    response = HTTParty.get('https://jsonplaceholder.typicode.com/users/47')
    content_type = response.headers['content-type']
    expect(content_type).to match(/json/)
  end
end

6.6 Modos de Gravação do VCR

Documentação

  • :once: Grava o cassete uma única vez, não recomendado para URIs não determinísticas. É configurado por padrão e chamado se nenhum outro modo for configurado.
    Exemplo:
    spec/httparty/httparty_spec.rb
it 'Content type - vcr' do
    VCR.use_cassette('jsonplaceholder/posts') do
      response = HTTParty.get('https://jsonplaceholder.typicode.com/users/2')
      content_type = response.headers['content-type']
      expect(content_type).to match(/json/)
    end
  end
  • :new_episodes: A cada nova URL, uma nova cassete será gravada.
    Exemplo:
    spec/httparty/httparty_spec.rb
  it 'vcr-new-episodes', vcr: { cassette_name: 'jsonplaceholder/posts', record: :new_episodes } do
    VCR.use_cassette('jsonplaceholder/posts') do
      response = HTTParty.get('https://jsonplaceholder.typicode.com/users/2')
      content_type = response.headers['content-type']
      expect(content_type).to match(/json/)
    end
  end
  • :none: Garante que não seja gravado nenhum cassete.
    Exemplo:
    spec/httparty/httparty_spec.rb
  it 'vcr-none', vcr: { cassette_name: 'jsonplaceholder/posts', record: :none } do
    VCR.use_cassette('jsonplaceholder/posts') do
      response = HTTParty.get('https://jsonplaceholder.typicode.com/users/2')
      content_type = response.headers['content-type']
      expect(content_type).to match(/json/)
    end
  end
  • :all: Quando configurado, grava cassetes para todas as requisições, mesmo as já gravadas anteriormente.
    Exemplo:
    spec/httparty/httparty_spec.rb
  it 'vcr-all', vcr: { cassette_name: 'jsonplaceholder/posts', record: :all } do
    VCR.use_cassette('jsonplaceholder/posts') do
      response = HTTParty.get('https://jsonplaceholder.typicode.com/users/2')
      content_type = response.headers['content-type']
      expect(content_type).to match(/json/)
    end
  end
  • :record_on_error: Quando configurado, não grava cassetes quando o teste falha.
    Exemplo:
    spec/httparty/httparty_spec.rb
  xit 'vcr-record_on_error', vcr: { cassette_name: 'jsonplaceholder/posts', record_on_error: false } do
    VCR.use_cassette('jsonplaceholder/posts') do
      response = HTTParty.get('https://jsonplaceholder.typicode.com/users/2')
      content_type = response.headers['content-type']
      expect(content_type).to match(/json/)
    end
  end

7. Time Helpers

Documentação

7.1 Configuração

spec/rails_helper.rb
```ruby
RSpec.configure do |config|
  config.include ActiveSupport::Testing::TimeHelpers
end

7.2 Métodos travel, travel_back e travel_to

  • :travel: Altera a hora atual para a hora no futuro ou no passado por uma determinada diferença de tempo, fazendo stub em Time.now, Date.today e DateTime.now.
    Exemplos:
Time.current     # => Sat, 09 Nov 2013 15:34:49 EST -05:00
travel 1.day
Time.current     # => Sun, 10 Nov 2013 15:34:49 EST -05:00
Date.current     # => Sun, 10 Nov 2013
DateTime.current # => Sun, 10 Nov 2013 15:34:49 -0500

Esse método também aceita bloco, que retornarão o current_time ao seu estado original quanto o bloco terminar.

Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
travel 1.day do
  User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00
end
Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
  • :travel_back: Retorna a hora atual de volta ao seu estado original, removendo os stubs adicionados por travel e travel_to.
    Exemplo:
Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
travel_to Time.zone.local(2004, 11, 24, 01, 04, 44)
Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
travel_back
Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
  • :travel_to: Altera a hora atual para a hora fornecida, fazendo stub em Time.now, Date.today e DateTime.now para retornar a hora ou data passada para este método.
    Exemplos:
Time.current     # => Sat, 09 Nov 2013 15:34:49 EST -05:00
travel_to Time.zone.local(2004, 11, 24, 01, 04, 44)
Time.current     # => Wed, 24 Nov 2004 01:04:44 EST -05:00
Date.current     # => Wed, 24 Nov 2004
DateTime.current # => Wed, 24 Nov 2004 01:04:44 -0500

Este método também aceita um bloco, que retornará a hora atual de volta ao seu estado original no final do bloco:

Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
travel_to Time.zone.local(2004, 11, 24, 01, 04, 44) do
  Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
end
Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00

7.3 Exemplos

Exemplo:
spec/models/customer_spec.rb

require 'rails_helper'

RSpec.describe Customer, type: :model do
  it 'travel_to' do
    travel_to Time.new(2004, 11, 24, 1, 4, 44) do
      @customer = create(:customer)
    end
    expect(@customer.created_at).to eq(Time.new(2004, 11, 24, 1, 4, 44))
    expect(@customer.created_at).to be < Time.now
  end
end

8. Gem TimeCop

Muito similar ao Time Helper do Rails, mas permite que seus métodos sejam utilizados com Ruby puro.
Documentação

9. Executando Testes em Ordem Aleatória

  • Na execução dos testes
    Exemplo: bin/rspec --order random
  • Configurando no .rspec
    Exemplo: --order random
  • Configurando no spec_helper.rb
    Exemplo: config.order = 'random'
  • Retornando a uma ordem de testes desejada
    Exemplo: bin/rspec --seed <numero-do-seed>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment