Duplas totalmente falsas para testes

Eu amo algumas simulações e esboços. Como muitas pessoas, eu realmente não tive
a idéia até que fiz algumas leituras sobre a técnica. E então eu realmente não entendi até que fiz mais algumas leituras . E mesmo assim eu realmente não entendi até que reescrevi um conjunto de testes 3 vezes, cada vez mais perto do nirvana da codificação: um grupo extremamente rápido de testes que expõe dependências e é resiliente a mudanças.

Um grande passo para mim foi implementar duplas de teste reais, afastando-me
de estruturas minúsculas e realmente criando uma classe Ruby para atuar como minha
dupla. A funcionalidade e a capacidade de reutilização dessas duplas são excelentes. Mas
eles apresentam um problema. Por exemplo, pegue este código idiota:

class Record
attr_accessor
:number

def initialize
#A ton of db work
end

def length
# even more db work
end
end

class RecordDouble
attr_accessor
:number

def length
@length ||= rand(50)
end
end

Agora posso usar o RecordDouble como colaborador em um teste e não preciso
incorrer na dor de criar uma instância de registro real. Yay.

class RecordCollaboratorTest
before
:each do
@record_double = RecordDouble.new
@it = RecordCollaborator.new(@record_double)
end

it
"should use record length in some way" do
assert_equal
@record_double.length, @it.length
end
end

E está tudo bem. Então eu mudo o método de comprimento no objeto Record:

class Record
...
def length(options_hash)
end
end

E uma variedade de coisas ruins acontecem, certo? Meus testes não documentam mais
como usar meu código. Meus testes são aprovados, mas a produção pode falhar.
deriva, e deriva = podridão = maldade.

E mesmo que isso nunca aconteça, usar dublês como esse é duplicação.
Tenho duas classes que têm (ou deveriam ter) a mesma interface. Cada
mudança em um precisa ser feita no outro.

Uma solução rápida que implementei quando fiz isso da primeira vez veio, mais
ou menos, do livro de Sandi Metz.

class RecordTest
setup
do
@it = RecordDouble.new
end

include
RecordInterfaceTest
...
end

class RecordDoubleTest
setup
do
@it = RecordDouble.new
end

include
RecordInterfaceTest
...
end

module RecordInterfaceTest
[:number, :id, length].each |m|
assert @it.responds_to?(m)
end
end

Ainda a duplicação (na verdade, agora a triplicação dos métodos API), mas pelo
menos eu poderia controlar a deriva. Mais ou menos. Se Record ou RecordDouble parasse de oferecer
suporte a um método que estava no teste de interface, uma campainha de alarme soaria
. Mas se arity mudasse, eu não notaria.

Avance vários meses e acabo implementando esse padrão
novamente em uma base de código diferente. Mas agora eu tenho este vídeo , no qual (na marca de 29 minutos), Sandi Metz aponta para quatro joias ( charlatão , fictício , rspec-fire , minitest-firemock ) que podem resolver esse problema de uma maneira melhor.

Depois de revisar as opções, escolhi Bogus. Seu uso parecia o mais limpo
e parecia ter uma documentação sólida. Acabei descobrindo que essa documentação está faltando um pouco, por isso esta postagem no blog.

Primeiro, estou implementando isso com a especificação MiniTest, e sua documentação
se inclina mais para o público Rspec. E estou testando objetos ActiveRecord,
enquanto sua documentação se concentra em código Ruby simples. Essas pequenas
rugas podem te fazer tropeçar. Eles certamente me fizeram tropeçar.

Colocando seu ‘test_helper’ em forma

Eu criei um novo arquivo auxiliar

ENV["RAILS_ENV"] = "test"
require File.expand_path('../../config/environment', __FILE__)
require 'minitest/autorun'
require 'bogus/minitest/spec'

Para o teste

describe Fruit do
fake
(:garden)
fake
(:trellis) { Support::Trellis }

describe
"#initialize" do
before
do
stub
(garden).fertilized? { true }
stub
(trellis).material { 'wood' }
end
...
end
end

Aqui, testarei um objeto Fruit. Tem dois colaboradores que preciso
dobrar: Jardim e Treliça. Aqui você vê as duas maneiras de chamar o
método falso do Bogus. No primeiro, não passo por nenhum bloqueio, então Bogus presume que eu quero
falsificar uma instância do Garden. Mas para treliça, eu passo na classe que
quero.

Todos os exemplos em Bogus usam o primeiro método. O segundo está
documentado
, mas eu estava tão ocupado olhando os exemplos que não percebi.

Agora a magia

O resultado das falsificações é que posso usar gardene trellisem
meus testes e métodos de stub nelas. E aquele diretório separado de
classes duplas ? Foi. A biblioteca de testes de interface? Foi. Tudo o que tenho agora são
as defições de objetos reais que finjo durante os testes.

E a magia tem um destacamento

Mas se eu tentar criar um esboço para algo que as classes não suportam, o Bogus gerará um erro.

stub(garden).disco_party? { true} => #NameError: #<Garden:0x3fea091c41b0> does not respond to disco_party?

E aridade é respeitada:

stub(trellis).leg_number(1, 'huh?') { nil } => ArgumentError: tried to stub leg_number(count) with arguments: 1,'huh?'

Mas talvez não seja mágico o suficiente?

Tudo isso é excelente e muito apreciado. Agora posso criar duplas
com facilidade e não me preocupar com o desvio da API. Vamos jogá-los em alguns
modelos ActiveRecord. Aqui está uma taxa que tem um código de valor que persiste
em um banco de dados:

irb> f = Fee.first
=> #<Fee code: 100>

Vamos dobrar isso e usá-lo em um teste:

describe FeeCollaborator do
fake
(:fee)

describe
"#initialize" do
before
do
stub
(fee).code { 999 }
end
...
end
end

E em vez de um duplo brilhante feliz, você obterá:

NameError: #<Fee:0x3fc3b60eed2c> does not respond to code

Mas Fee obviamente responde ao código, acabei de ver no console! E
é aqui que está a documentação falsa, desculpe-me por dizê-la,
um pouco falsa. Em Opções de configuração, há um link para
Falso ar atributos que lhe dirá por que isso está quebrando e como corrigi-lo. Resumindo, basta colocar isso em seu arquivo test_helper inicial:

Bogus.configure do |c|
c
.fake_ar_attributes = true
end

E seus testes de objetos ActiveRecord funcionarão.

Há muito mais recursos Bogus que estou ansioso para experimentar.
Espiões, contratos e comparadores de argumentos parecem ótimos. E se eu encontrar algum
truque para implementar esses recursos, vou postá-lo aqui.