O guia definitivo sobre dublês de teste no Android — Parte 1: Teoria
Mocks, stubs, fakes, dummies e spies no Android: da teoria às (boas) práticas
Utilizar dublês de teste da forma correta é fundamental para qualquer estratégia de testes automatizados, independente da tecnologia ou do produto no qual você esteja trabalhando. No Android em especial, a utilização desse recurso se torna ainda mais importante quando tratamos da parte não-instrumentada da suíte de testes. Em essência, o conceito que explica os dublês de teste é bem simples, porém a grande quantidade de nomenclaturas e de ferramentas disponíveis causam muita confusão na comunidade de desenvolvimento, você já deve ter ouvido frases do tipo:
- "É só mockar essa dependência que tudo vai dar certo" 🙌
- "Evite usar Mocks!" 😱
- "Mocks vs Stub?" ⚔️
- "Melhor usar Fakes do que Mocks" 🤔
Por incrível que pareça, as frases acima podem ser interpretadas de maneira diferente se não soubermos a definição correta das coisas. Se você nunca ouviu falar sobre dublês de teste ou quer se aprofundar mais sobre o assunto, segue lendo que tudo será explicado!
O que são dublês de teste?
Antes de explicar, gostaria de relembrar algumas características básicas de um bom teste unitário, elas são: rapidez, determinismo e fácil configuração. Com isso em mente podemos pensar na seguinte definição para dublês de teste:
Os dublês de teste são substitutos que sobrepõem dependências necessárias para se testar um sistema ou um comportamento. Os dublês de teste são melhor aplicados quando substituem dependências lentas, não-determinísticas ou de difícil configuração.
O conceito de dublês de teste foi criado pelo Gerard Meszaros no livro XUnit Test Patterns: Refactoring Test Code e refinado por muitas outras referências na área de testes e engenharia de software. Na literatura podemos encontrar 5 categorias de dublês de teste, cada uma com o seu propósito específico:
Dummy
O Dummy é o dublê mais simples de todos. Ele possui o único propósito de configurar um teste, ao mesmo tempo que não apresenta muita relevância para o mesmo.
Dummies são muito utilizados para preencher parâmetros obrigatórios e nada mais que isso. Normalmente não precisamos de ferramentas adicionais para criá-los.
No exemplo acima, o dummyNote
tem o único propósito de ser passado por parâmetro e seu conteúdo não é tão relevante para o teste. Abaixo temos alguns exemplos de variáveis que também podem ser consideradas Dummies:
Por sua simplicidade, os Dummies ajudam a manter o código pequeno, limpo e livre de ferramentas externas. Use outro dublê de teste apenas se for realmente necessário.
Stub
O Stub é um dublê de teste que provê respostas fixas ou pré-configuradas para substituir a implementação real de uma dependência.
Os Stubs normalmente são gerados por ferramentas agregadas ao seu projeto mas também podem ser implementados manualmente. Eles evitam que chamadas lentas ou não-determinísticas sejam feitas durante a execução do teste.
No caso acima, utilizamos o MockK para configurar o Stub. Quando utilizamos ferramentas externas com esse propósito, tendemos a acoplar a configuração do dublê à implementação da dependência. A linha 9 do exemplo acima demonstra isso, quando configuramos exatamente o que a dependência notesApiStub
precisa responder quando o fetchAllNotes()
for chamado:
every { notesApiStub.fetchAllNotes() } returns listOf(note, note)
Essa resposta pré-configurada fará com que uma lista de duas notas seja retornada imediatamente, evitando a chamada para um backend real. Use Stubs quando você precisar de respostas rápidas, determinísticas e pré-configuradas para seu teste.
Fake
O Fake é um dublê que tem um propósito muito parecido com o do Stub: prover respostas simples e rápidas para um cliente que o consome. A principal diferença é que o Fake utiliza uma implementação funcional por debaixo dos panos.
O Fake normalmente implementa a mesma interface da dependência que ele substitui. Sua principal característica é possuir uma implementação funcional e um pouco mais inteligente do que a dos Stubs, não retornando somente respostas pré-definidas configuradas previamente. Por esse motivo, ele se aproxima mais do comportamento real do sistema do que outros dublês de teste.
Um ponto interessante é que os Fakes não precisam saber da implementação concreta de quem ele substitui. Isso faz com que detalhes internos das dependências não vazem para o teste. Diferentemente do exemplo com o Stub, no exemplo acima não precisamos saber que o colaborador noteApiFake
tem um método chamado fetchAllNotes()
já que esse comportamento está encapsulado dentro do próprio Fake.
Um Fake muito famoso que podemos encontrar no mundo Android é o banco de dados em memória do Room. Apesar de fazer uso de uma ferramenta externa para criá-lo, ele ainda pode ser considerado um dublê de teste Fake pois o mesmo possui uma implementação funcional que substitui a do banco de dados real.
val database = Room.inMemoryDatabaseBuilder(
context,
MainDatabase::class.java
).build()
Use Fakes quando você precisar de respostas rápidas e determinísticas para seu teste e ao mesmo tempo não queira vazar detalhes de implementação das dependências. Eles também poderão ser usados quando você precisar reproduzir uma resposta mais complexa na qual os Stubs não dariam conta ou quando você ainda não tiver uma dependência implementada e precisar dela para algum teste. Os Fakes são muito utilizados em testes que estão nas fronteiras de I/O do sistema, substituindo dependências que normalmente compartilham estado, como bancos de dados e backends.
Mock
O Mock é um dublê que tem como objetivo verificar interações específicas com dependências durante a execução de um teste. Em outras palavras, podemos dizer que os Mocks substituem dependências que queiram ser observadas quando um sistema estiver sendo testado.
Os Mocks não necessariamente precisam configurar um retorno hard-coded como os Stubs. Eles normalmente são criados por ferramentas agregadas ao seu projeto mas também podem ser criados manualmente.
No exemplo acima, utilizamos o MockK para configurar um Mock. Nele precisamos verificar que o NoteAnalytics
chama o método AnalyticsWrapper.logEvent(String, String)
para finalizar o procedimento trackNewNoteEvent(Enum)
. Observar essa interação com a dependência é algo que o Mock se propõe a fazer.
verify(exactly = 1) {
analyticsWrapperMock.logEvent("NewNote", "SuperMarket")
}
Perceba que o nome do método e seus respectivos parâmetros são detalhes de implementação do AnalyticswWrapper
que são exibidos durante a configuração do teste do NoteAnaytics
. Assim como os Stubs, os Mocks gerados por ferramentas também precisam expor esses detalhes para serem configurados corretamente. Apesar dessa semelhança, ambos possuem propósitos bem diferentes. O propósito do Mock é observar e verificar uma interação com a dependência, enquanto o do Stub é simular um comportamento da dependência e retornar valores pré-definidos.
Use Mocks quando você precisar verificar interações específicas com dependências, especialmente se o comportamento testado não possuir retorno (sendo ele Void ou Unit). Evite utilizar Mocks diretamente em classes que você não tem controle sobre a implementação, como em bibliotecas externas, já que o contrato pode mudar a qualquer momento e isso pode se transformar em um problema de compilação no seu teste no futuro. Nesses casos, busque criar Wrappers que encapsulam essas dependências externas sobre as quais você não tem controle e crie Mocks para os Wrappers.
Spy
Na minha opinião o Spy é o dublê mais confuso de todos, pois sua definição varia um pouco entre diferentes autorias.
Podemos dizer que o Spy tem um propósito parecido com o do Mock, que seria observar e verificar interações com as dependências durante a execução de um teste, a diferença é que o Spy se utiliza de uma implementação funcional para operar.
Um Spy pode implementar a mesma interface da dependência que ele irá substituir ou estender a implementação concreta dessa dependência sobrescrevendo algum método para registrar alguma informação relevante para a verificação do teste. Independentemente se o Spy é gerado por uma ferramenta ou criado manualmente, por definição sempre existirá uma implementação funcional por debaixo dos panos.
No exemplo acima, temos um Spy que foi implementado manualmente. O que definirá se o teste passou ou não será o que estiver implementado dentro do método assertThatNewNoteEventWasRegistered(String)
. Nessa assertiva, queremos verificar que um evento de analytics foi disparado com um certo parâmetro específico, ou seja, verificar que a interação do NoteAnalytics
com sua dependência ocorreu dessa maneira.
Você encontrará definições diferentes para Spy durante seus estudos, até mesmo a definição do próprio Martin Fowler é um pouco confusa para mim quando tentamos diferenciar Spies de Mocks. Em algumas situações, fazer uma diferenciação entre Spies e Mocks no código pode ser mais confuso do que útil. Para tentar amenizar essa um pouco a confusão, minha recomendação é que você foque que os Spies observam interações com dependências e que possuem implementação funcional.
Os Spies devem ser usados quando você queira verificar que sua dependência está em um estado específico e que você não conseguiria fazer isso de forma simples com um Mock. Fazer várias verificações em um único teste pode ser uma evidência que você está tentando observar um estado complexo. Você também pode usar um Spy caso queira deixar suas verificações mais significativas (através de métodos customizados e bem nomeados).
Dummies, Stubs, Fakes, Mocks e Spies: um resumo
Depois de explicar os conceitos, vocês puderam perceber que não é à toa que as pessoas têm dificuldades com os dublês de teste, são muitas nuâncias que definem cada um deles. Para resumir e facilitar a explicação, podemos dividir os 5 dublês nas seguintes categorias:
- Aqueles que não simulam comportamento nem observam interações: Dummies.
- Aqueles que simulam comportamento: Stubs e Fakes.
- Aqueles que observam interações: Mocks e Spies.
- Aqueles que não necessariamente possuem implementação funcional: Dummies, Stubs e Mocks.
- Aqueles que necessariamente possuem implementação funcional: Fakes e Spies.
Todos os dublês podem ser gerados manualmente ou por ferramentas externas. Os dublês que possuem comportamento configurado por ferramentas tendem a acoplar detalhes de implementação das dependências no teste, isso traz uma facilidade inicial no uso mas pode gerar um maior custo de manutenção no futuro. Os dublês gerados manualmente tendem a dar mais trabalho para implementar no início mas deixam os testes mais fáceis de manter no longo prazo.
Por quê as pessoas chamam todos os dublês de Mocks?
Não serão raras as vezes que você verá as pessoas chamarem de Mocks quando na realidade querem se referir a Stubs (ou outros dublês). Por questões de simplicidade, todos os dublês são comumente chamados apenas de Mocks. Isso de deve porque as ferramentas que criam dublês de teste generalizaram esse termo.
Veja o exemplo do MockWebServer, MockK e Mockito. Independentemente se o dublê é um Mock, um Stub ou um Fake, o nome Mock é o que normalmente vem à tona. Um motivo para isso é que alguns dublês podem assumir papéis duplos, sendo Mocks e Stubs ao mesmo tempo. Nesses casos, se tornou preferível chamar dublês mais genéricos ou que possuem múltiplos papéis de Mocks do que criar outra nomenclatura para eles.
Outro motivo para a generalização do termo se deve a existência de definições inconsistentes que aparecem em livros e artigos na internet:
“Classification between mocks, fakes, and stubs is highly inconsistent across the literature.[1][2][3][4][5][6]”
— Retirado de Wikipedia, Mock Object
Ao meu ver, não é um problema tão grave resumir os dublês a Mocks, pois isso pode ajudar na comunicação e na diminuição da carga cognitiva. Ao mesmo tempo, acredito que seja muito importante para quem utilize esse termo saiba que ele se trata de uma abstração.
As duas escolas de testes unitários e o papel dos dublês em cada uma delas
Como dito no início desse artigo, a utilização dos dublês da forma correta é de extrema importância para a parte não-instrumentada da pirâmide de testes. A maneira como você os utiliza depende de qual escola de testes você segue:
- A escola dos testes sociáveis. Também conhecida como escola clássica, Detroit style ou Chicago Style.
- Ou a escola dos testes solitários. Também conhecida como estilo mockista ou London style.
Talvez você construa testes utilizando uma dessas escolas e nem se dê conta disso. A principal diferença entre elas é a definição de unidade, que por consequência define o que é um teste unitário.
Para a escola dos testes sociáveis (Detroit Style), a unidade é representada pelo comportamento, independentemente se esse comportamento é composto por mais de uma classe ou não. Nesse caso, um teste unitário pode ser feito com múltiplas classes reais, contanto que elas representem um único comportamento e o teste consiga ser rápido e paralelizável. A recomendação aqui é utilizar dublês para substituir dependências que compartilhem estado, que sejam muito lentas ou que estejam fora do seu controle, ex: banco de dados, backend, ferramentas de terceiros (utilizando padrão wrapper).
Para a escola dos testes solitários (London Style), a unidade é representada pela classe. Nesse caso, todas as dependências dessa classe cujos contratos são controlados por nós devem ser substituídas por dublês, mesmo que sejam rápidas e fáceis de configurar. A intenção desse estilo de teste é alcançar um nível maior de isolamento e controle.
Conclusão
É isso! Agora que você aprendeu a teoria que envolve a utilização dos dublês de teste, hora de passar para a segunda parte dessa série, nela iremos falar sobre pontos específicos do Android:
Se você aprendeu algo com esse artigo, deixe suas palmas 👏 Assim eu posso saber que ele foi útil para você. Qualquer feedback ou dúvida, manda no Twitter ou nos comentários desse post. Obrigado!
Um agracedimento especial a Fred Porciúncula, Felipe Martins e Angélica Oliveira por revisarem esse artigo.