Desenvolvimento

17 nov, 2016

Por que mocks falham?

Publicidade

Em um dos seus posts, Yegor mostra que construir mocks que retornam outros mocks (e que podem retornar outros mocks) pode ser extremamente problemático. Como solução do problema, ele apresenta fake objects que são muito parecidos com algo que o Uncle Bob já tinha falado: criar objetos com a mesma interface, mas com implementação em memória.

Mas será que deveríamos mesmo escrever mocks que retornam outros mocks? E será que não criar nenhum mock é de fato a solução?

A grande virtude da POO

John Cinnamond numa palestra recente chega a uma conclusão brilhante a respeito de POO: ela é uma ferramenta usada para desconstruir um problema complexo e postergar decisões (implementações). E usando-a você (seu time, empresa, que seja) pode se concentrar em um problema por vez.

Mais que uma ferramenta, POO une dados e comportamento. Uma ferramenta que possibilita a criação (ou adoção de regras de um dado domínio) das nossas próprias verdades (invariants) e protocolos (contratos, interfaces definindo a interação entre objetos, times e clientes). Apesar de não ser uma virtude única do paradigma, os testes que escrevemos são uma maneira de documentar e validar nossos protocolos, e eles são uma ferramenta ainda melhor para postergar decisões.

Mocks postergando decisões

Quando estamos escrevendo um teste (e mais ainda quando fazemos TDD), mocks podem substituir interações complexas (sim, essas que a POO possibilita) ou colaboradores que ainda não existem, livrando-nos de ter que pensar nos detalhes de como algo vai ser feito. Usando o exemplo do post:

require 'spec_helper'
  
describe Employee do
  describe '#salary' do
    it 'return amount based on employee region' do
      region = double
      expect(region).to receive(:salary_by)
                        .with('Nelson').and_return(5000)
      employee = Employee.new('Nelson', region)
      
      expect(employee.salary).to eq(5000)
    end
  end
end

No exemplo acima, você não precisa saber o que é region ou como ele obtém a informação de salário – o importante aqui é descrever como a interação entre os colaboradores acontece.

Pra mim, a razão de usar mocks é: representar a interação entre objetos programando para uma interface, e não para uma implementação.

Programar pra uma interface permite que a dependência de region seja substituída por qualquer outra coisa que responda à mensagem salary_by (posso criar um fake object para esconder a implementação de region se quiser também) e postergar a decisão de ter que escolher um ORM, banco de dados ou biblioteca e até mesmo deixar que outro time faça essa escolha depois.

Você só pode postergar o que é seu

Mas para ter todas essas vantagens com mocks (ou fazê-los brilharem), você precisa ter controle sobre eles, e é aí que mocks dão errado…

O artigo Mock Roles, not Objects estabelece um conjunto de regras simples (não são bem regras… são guidelines) pra mocks, e a mais importante (e a mais ignorada na maioria dos casos também), IMO, é: “Only Mock Types You Own” (ou só crie mocks para tipos criados por você). Seguindo esse conselho, você sempre criará mocks pra uma interface criada por você, que geralmente é muito mais simples que uma exposta por uma biblioteca ou framework (por causa do nível de abstração necessário).

Interfaces simples são mais simples de testar e manter (menos métodos públicos, menos acoplamento e maior coesão).

Então, se você perceber que está criando um mock que retorna outro mock, ou que está criando um mock pra uma biblioteca de terceiros, pense duas vezes! Confira se a operação não pode (ou deveria) ser encapsulada em outra interface (talvez seja um conceito de domínio faltando na sua aplicação) e, principalmente, não tenha medo de mudar coisas se você acha que o mock hell está batendo na sua porta (ou pode aparecer em breve).

Happy testing!