Publicações

Conceitos de Mocks e Stubs em Teste de Unidade

Lucas Rodrigues
Lucas RodriguesBackend Developer
Publicação:  12-01-2023

Introdução

Testes de Unidade ou teste unitários são importantes para o desenvolvimento de qualquer sistema. Essa é uma máxima já bastante conhecida entre os desenvolvedores, e por isso não irei me estender sobre essa importância. O que pretendo é pensar sobre a ideia de ‘Mocks’ e ‘Stubs’. O que são esses conceitos, quais os objetos usados para aplicá-los e quais resultados eles proporcionam. Pois conhecendo esses conceitos podemos entender quais tipos de verificações nossos testes estão fazendo e o que de fato estamos testando em nosso sistema. Pretendo não descrever um tutorial sobre ‘Mocks’ e ‘Stubs’, mas buscar um entendimento conceitual sobre eles.

teste de unidade

Teste de Unidades e os Test Doubles

Testes de unidade são aqueles que se propõem a verificar pequenas partes do sistema para que possamos garantir um funcionamento mínimo, evitar bugs e até ter um melhor entendimento de determinada a parte/funcionamento do sistema. Diferente dos testes ponta a ponta, ou os outros tipos de teste, estamos pensando em uma verificação de uma funcionalidade específica, de algo contido no sistema, e não o sistema como um todo.

dependencia do componente

De qualquer forma, testes de unidade nem sempre são totalmente isolados. Muitas vezes podemos ter a situação em que determinado método utiliza algum recurso externo ou faz uma chamada a uma API, para concluir sua função. Podemos então denominar a parte que queremos testar como SUT (System Under Test), e esses recursos externos podemos chamar de colaboradores

Os colaboradores podem ser qualquer recurso, que é utilizado pelo SUT, para realizar a sua funcionalidade. Recursos esses que podem ser uma API, uma consulta a uma base de dados, um serviço de cloud, uma biblioteca, uma outra função ou classe do próprio sistema. E, diferente de um teste de integração, o teste de unidade não tem necessidade de que os seus colaboradores estejam funcionando plenamente, ou que tenham uma constância nos resultados das ações ou respostas dos colaboradores. Não serão eles que estarão sendo testados. Eles existem como parte auxiliar para que o método possa completar seus processos, e possuem uma lógica própria que pode, em muitas vezes, trazer um novo dado ou transformar algo dentro do SUT. E outras vezes pode apenas disparar uma ação, ou abrir um outro processo. Mas simplificando, temos uma entrada e uma saída de informação. E são essas partes que interessam ao SUT.

Outro ponto que os colaboradores podem influenciar nos testes de unidade é a velocidade do teste em si. Se o colaborador precisar de um tempo maior para resolver a sua parte, assim também o SUT levará mais tempo para concluir sua execução. 

Para evitar esses pontos, podemos utilizar representações simuladas dos colaboradores e assim controlar melhor o processo que está sendo testado. Essas simulações de recursos externos ao SUT podem ser chamadas de ‘Dublês’ de teste (Test Double) de forma genérica. E podem ser subdivididos em Dummy, Fake, Stubs, Spies e Mocks. Tendo eles pequenas diferenças em relação ao comportamento que essa simulação terá com o SUT. Gerard Meszaros (autor do livro ‘XUnit Test Patterns: Refactoring Test Code’) definiu cada tipo de Double da seguinte forma:

  • Dummy: Objetos que são passados, mas não são utilizados. Geralmente são apenas usados para preencher lista de parâmetros
  • Fake: Objetos que têm uma implementação funcionando, mas geralmente usam algum atalho que os torna inadequados para produção (um banco de dados na memória é um bom exemplo).
  • Stubs: Retorna respostas prontas para chamadas feitas durante o teste, geralmente não respondendo a nada fora do que está programado para o teste.
  • Spies: São como ‘stubs’ mas que também registram algumas informações com base em como foram chamados. Uma forma disso pode ser um serviço de e-mail que registra quantas mensagens foram enviadas.
  • Mocks: Objetos pré-programados com expectativas que formam uma especificação das chamadas que se espera que recebam.

A grande vantagem dos ‘test doubles’ é que nenhuma conexão com o colaborador real é executada. O ‘test double’ irá substituir esse colaborador real por uma lógica mais simples e que não depende de recursos externos ao teste, pois a implementação estará na própria suíte de teste.

esquema de representaçao de teste de unidade

Verificação de Estado e de Comportamento

E o que queremos testar em nossa unidade, em nosso SUT? Essa é uma pergunta central para qualquer teste. Só podemos ter a certeza que nossos testes estão assegurando nosso código se soubermos o que precisa ser testado. E saber exatamente o que deve ser testado é o melhor caminho para saber qual ferramenta utilizar na implementação do teste. Sobre isso podemos começar a pensar se queremos verificar o estado que o processo gerou ou se queremos verificar o comportamento do processo.

Uma validação de estado afirma que o valor de uma propriedade, variável ou resposta é igual à expectativa. Um exemplo disso pode ser verificado se uma função retorna o valor que desejamos, ou talvez se recebemos o conjunto de registros esperado ao consultar um banco de dados.

Enquanto isso, uma validação de comportamento provavelmente irá além da validação de estado, porque também verificará se as ações e eventos esperados dentro do SUT foram acionados pelo teste. Um exemplo de validação de comportamento é afirmar se um método foi chamado, quais parâmetros foram passados, ou quantas vezes aquele método foi chamado.

Mock

Mocks. A definição de Gerard Meszaros para mocks já diz bastante sobre. São objetos pré-programados que formam uma especificação das chamadas que esperam receber. Ou seja, são objetos que simulam o colaborador verificando se um ou mais métodos foram ou não chamados, a ordem de chamadas destes métodos, se esses métodos foram chamados com os argumentos certos, e quantas vezes foram chamados. Dessa forma conseguimos verificar o comportamento do SUT e perceber se algum comportamento pode causar algum problema na execução do sistema. Verificando assim o processo durante a execução e não apenas o resultado final. Esse modo de verificação é interessante para sistemas que possuem chamadas a colaboradores que não devolvam nenhum resultado, como por exemplo um envio de email ou mensagem, o acionamento de um trigger ou qualquer processo que é apenas iniciado no SUT sem ter a responsabilidade de retornar algo para o sistema que é testado.

Stub

Stubs por sua vez é uma opção para testes que podem verificar somente o estado do sistema. Isso porque o stub tem a única função de retornar um valor programado para simular o retorno do colaborador. Não é importante saber como os argumentos foram passados para o colaborador ou quantas vezes ele foi chamado, somente o seu retorno. A verificação é pela alteração do estado que o SUT vai ter ao longo do teste. Esse será o ponto importante da verificação que o teste estará fazendo, se o estado que iniciou o teste, foi alterado para o que se espera ao final do teste. 

Mock em Python

Todos esses conceitos apresentados aqui, podem ser usados em qualquer linguagem. São entendimentos mais abrangentes que não são atributos de um framework ou implementação específica. Quando vamos procurar entender as ferramentas que iremos utilizar nos testes, podemos ter certas diferenças em uma nomenclatura, por exemplo. Mas o conceito ainda estará presente.

No caso da linguagem Python, temos o framework unittest, nativo da linguagem. Nesse framework temos o objeto Mock. Esse objeto, como o próprio nome já diz, tem a capacidade de servir como o ‘test double’ mock, ou seja, pode servir para verificar o comportamento do SUT. E apesar de não existir o objeto Stub no framework unittest, podemos utilizar o próprio objeto Mock para ele atuar apenas como um stub, ou seja, apenas retornar o valor esperado. Mesmo o objeto Mock tendo a possibilidade de verificar o comportamento, ele é apenas utilizado para a verificação de estado.. Então por que diferenciar esses conceitos? Para termos a certeza de que nosso teste está abrangendo o que for necessário para assegurar a confiabilidade do sistema. Para simular diferentes cenários e entender como o nosso sistema é capaz de lidar com exceções, por exemplo.  Podemos testar condicionais, e os diferentes caminhos que uma função pode tomar, testar se algum método foi chamado e quantas vezes, assim como quais parâmetros utilizados. Podemos não somente verificar, mas simular situações para verificar como a função responde, por exemplo levantando uma exceção para testar como a função se resolve nessa situação.  

Um exemplo de teste utilizando o objeto Mock do framework unittest, em que verificamos quais argumentos foram passados para o colaborador durante o teste:

import mock
 
def test_table_exists(worker: Worker):
   """
      O método Mock retorna uma função falsa
   """
   worker.client.query = mock.Mock(return_value=[{'a': 1},{'b': 2}])
   worker.table_exists('database_name')

   assert worker.client.query.call_args_list == [
      (('SELECT * FROM database_name',),)
   ]

Conclusão

Entender os conceitos de Test Doubles vai facilitar a construção de testes mais eficientes, com uma cobertura de possíveis cenários e condições, e dar a certeza que o sistema funciona como esperado. É ter o conhecimento de que o teste irá garantir que novas implementações não irão conflitar com o que já está devidamente testado. Testes bem desenhados podem inclusive trazer o entendimento do sistema para quem ainda não o conhece, pois conseguirá demonstrar qual o propósito de tal função no sistema.

Este artigo não pretendia detalhar tecnicamente os objetos e ferramentas utilizadas para compor os testes, seus Mocks e Stubs. Existem diversos artigos excelentes com esse intuito. Disponho alguns links de referência sobre e alguns artigos mais técnicos ligados à linguagem Python. O Objetivo era trazer um entendimento mais alto nível para auxiliar na composição de qualquer teste de unidade, e também do entendimento sobre o sistema, quais as necessidades e responsabilidades de cada parte. Acredito também que com esse entendimento seja mais fácil pesquisar e conhecer novas ferramentas utilizadas para testes de unidade.

Alguns artigos que utilizei

Alguns artigos mais especificos para testes em Python

Solução de problemas, melhoria de gestão e aumento de produtividade

Com tecnologias renomadas, aplicamos práticas ágeis e capacitamos sua equipe.

Fale com um consultor