Lembro-me bem da primeira vez que ouvi o termo. Era meados de 2009, quando li em algum lugar do GUJ sobre um "recurso" muito valioso nos Testes Unitários que era de fundamental entendimento para seguir adiante. Depois de ler algumas coisas na Internet, eu havia encontrado pelo menos três diferentes definições para Mock e Stub. Naquele momento eu descobri de que precisava de bibliografias-referência no assunto para acabar com aquelas meias verdades que pairavam sobre minha cabeça.
A primeira definição que encontrei era mais ou menos assim: "Mock você utiliza quando o valor de retorno importar; Stub nos demais casos." - ah, quantas vezes não "testei" software com esse mindset. Mesmo não entendendo bem o que era o tal valor de retorno, eu me aventurava e me forçava a fazer. Segui assim até ler pela primeira vez o livro Growing Object-Oriented Software, Guided by Tests, de Steve Freeman e Nat Price. Naquele momento minha cabeça explodiu e tudo fez mais sentido. Desde então, costumo definí-los da seguinte forma:
Stub é uma dependência da sua classe-alvo (Objeto em Teste), agindo como um substituto, evitando que a implementação real seja executada.
Explicação longa:
class Authenticator
def login(user)
return user.password == "123456"
end
end
describe Authenticator do
it "will login with valid credentials" do
user = double('User', password: '123456')
expect(subject.login(user)).to be_true
end
end
Repare no teste o user = double('User', password: '123456')
e repare que isto permite que eu simule um usuário "válido" (no exemplo é só o password bater com 123456) - ou seja, eu configurei minha dependência (User) para que o objeto Authenticator
pudesse ter um usuário válido. Um exemplo mais elaborado seria:
class Authenticator
def login(user)
if (user.admin? and user.has_confirmed_account?)
self.grant_permissions_to(user)
else
false
end
end
private
def grant_permissions_to(user)
# do something nice with our lovely user
end
end
class User
def initialize(sms_api: SMSApi.get_instance)
@sms_api = sms_api
end
def admin?
type == 'admin' # type could be an Database field mapped
end
def has_confirmed_account?
user.documentation_already_approved? and sms_api.cellphone_confirmed_for(self)
end
private
def sms_api
@sms_api # SMS Wrapper Injected via initialize (constructor) method
end
end
describe Authenticator do
it "will login with valid credentials" do
user = double('User', :admin? => true, :has_confirmed_account? => true)
expect(subject.login(user)).to be_true
end
end
Deixei o exemplo mais elaborado para mostrar o poder e a importância do Test Double: note que stubando o método User#has_confirmed_account?
eu simplesmente evito ter que lidar com o SMSApi
e com o documentation_already_approved?
, bastando eu ter feito meu double retornar true no has_confirmed_account?
. Imagina o trabalho que eu teria para configurar o SMSApi e o método de documentação aprovada em todo teste que eu precisasse chamar o método has_confirmed_account?
. Insano, né?
Graças ao double eu consigo focar no meu problema que é: o Authenticator#login
está, dado um usuário aceito por ele, conseguindo autenticar este user object?
Repare que o método grant_permissions_to(user)
não é stubado. Ele precisa ser chamado de verdade pois é um colaborador interno da classe-algo (Authenticator class).
Mock é ligeiramente diferente, precisa estar atento para entender as diferenças.
O Mock irá criar a expectativa de que aquilo que você definiu irá de fato acontecer. Se não acontecer, o teste falhará.
describe Authenticator do
it "will login with valid credentials" do
user = double
expect(user).to receive(:admin?).once.and_return(true)
expect(user).to receive(:has_confirmed_account?).once.and_return(true)
expect(subject.login(user)).to be_true
end
end
Assumindo o exemplo anterior, modifiquei apenas o teste, trocando Stub por Mock. Trocando quando e se, encontrar user.admin? e/ou user.has_confirmed_account?, substitua por true e true respectivamente para você (Authenticator#login) deverá chamar user.admin? e user.has_confirmed_account? (em qualquer ordem no método) apenas uma vez (once), e terá true e true respectivamente como resposta. Saímos de algo simples, para algo assertivo. Se por um acaso eu trocar o código de produção para:
class Authenticator
def login(user)
if (user.has_confirmed_account?)
self.grant_permissions_to(user)
else
false
end
end
end
O teste neste caso começará a falhar, reclamando a falta do user.admin?
.
Naturalmente, há regras e boas práticas para quando testar expectativas via Mock's e quando não. Sandi Metz abordou o tema neste Lunch 'n Learn.
Mock/Stub parciais (Partial Mocks)
Um recurso suportado pelo RSpec são os Partial Mocks. Há quem defenda o não uso deles (Prophecy @ PHPSpec, estou olhando para você!). Mock parcial, permite que você utilize um objeto real como dependência e mock apenas determinados métodos dela. Ainda continuando com o exemplo do autenticador, teríamos:
describe Authenticator do
let(:admin_user) { User.new(...) # faz alguma coisa para construir um Usuário admin? == true}
it "will login with valid credentials" do
expect(admin_user).to receive(:has_confirmed_account?).once.and_return(true)
expect(subject.login(admin_user)).to be_true
end
end
As diferenças aqui são:
- Não utilizamos um
double
do RSpec. Preferimos utilizar o User object de verdade, fazendo o que for necessário para criar/retornar um usuário administrador, ou seja, um objeto deUser
cujo o método#admin?
retornará true sem a necessidade de mudar o valor de retorno do método com stub. - Com isto, não precisamos definir no teste
it...
o retorno de#admin?
(pude remover a linha)
Com isto, ainda assim, eu mockei o #has_confirmed_account?
para continuar retornando true. Com isto, acabei fazendo um Mock Parcial: o método #admin?
é chamado de verdade e o #has_confirmed_account?
é mockado para retornar sempre true naquele teste.
Concluíndo
Mocks e Stubs são fundamentais para a construção do seu design e para seguir em frente com Test-Driven Development. Neste post, busquei mostrar, com definições mais simples, o que são ambos e dar foco nas suas diferenças. Mas não pense que sair mokando/stubando tudo é boa prática. Há situações onde você não deve mockar; há situações onde o mock aponta um possível problema de design - e então você precisa refatorar seu código e talvez criar uma nova layer na aplicação. Em todo caso, pratique muito o assunto e torne seu código mais legível, testável e plugável.
Happy Mocking ;)
0sem comentários ainda