Mocking em Python 101: finja antes de fazer
10 de fevereiro de 2018
0 minutos de leituraNota do editor
Este blog foi publicado originalmente em fugue.co. A Fugue se juntou à Snyk em 2022 e é um componente essencial do Snyk IaC.
FugueBem-vindo a este guia sobre os conceitos básicos de mocking em Python. Ele surgiu da minha necessidade de testar um código que usava muitos serviços de rede e da minha experiência com GoMock, que me mostrou como o mocking pode ser poderoso quando feito corretamente (obrigado, Tyler). Vou começar com uma discussão filosófica sobre mocking, porque fazer bons mocks exige uma mentalidade diferente daquela necessária para um bom desenvolvimento. Desenvolver é criar coisas; fazer mocking é fingir coisas. Isso pode parecer óbvio, mas o aspecto de “fingir” nos testes com mocks é profundo, e compreendê-lo por completo muda a forma de encarar os testes. Depois, vamos conhecer as ferramentas de mocking oferecidas pelo Python e, por fim, veremos um exemplo completo. Saiba mais sobre como testar código para segurança em Python com nosso guia de referência.
Pode ser difícil entender o mocking. Quando testo um código que escrevi, quero saber se ele faz o que deveria de ponta a ponta. Geralmente começo pensando em um teste funcional e integrado, no qual insiro dados realistas e obtenho resultados realistas. Acesso todos os sistemas reais usados pelo meu código para garantir que as interações entre eles funcionem corretamente, usando objetos reais e chamadas de API reais. Embora esses testes sejam essenciais para verificar se sistemas complexos estão funcionando bem em conjunto, não é isso que queremos dos testes unitários.
Os testes unitários se concentram na camada mais externa do código. Testes de integração são necessários, mas os testes unitários automatizados que executamos não devem chegar a esse nível de interação entre sistemas. Isso significa que chamadas de API na função que estamos testando podem e devem ser substituídas por mocks. Devemos substituir qualquer chamada de API ou criação de objeto não trivial por uma chamada ou um objeto mockado. Assim, evitamos o uso desnecessário de recursos, simplificamos a instanciação dos testes e reduzimos o tempo de execução. Pense em um teste para uma função que acessa uma API HTTP externa. Em vez de garantir que um servidor de teste esteja disponível para enviar as respostas corretas, podemos mockar a biblioteca HTTP e substituir todas as chamadas HTTP por chamadas mockadas. Isso reduz a complexidade e as dependências dos testes, além de nos dar controle preciso sobre o que a biblioteca HTTP retorna — algo que pode ser difícil de conseguir de outra forma.
O que queremos dizer com mocking?
O termo mocking é muito usado, mas neste documento adotamos a seguinte definição:
“A substituição de uma ou mais chamadas de função ou objetos por chamadas ou objetos mockados”
Uma chamada de função mockada retorna imediatamente um valor predefinido, sem executar nenhum trabalho. Da mesma forma, os atributos e métodos de um objeto mockado são definidos inteiramente no teste, sem criar o objeto real nem executar qualquer trabalho. O fato de quem escreve o teste poder definir os valores retornados por cada chamada de função dá a essa pessoa um enorme poder durante os testes, mas também significa que é preciso fazer um trabalho de base para configurar tudo corretamente.
Em Python, o mocking é feito por meio do módulo unittest.mock. O módulo contém várias classes e funções úteis, das quais as mais importantes são a função patch (que pode ser usada como decorador ou gerenciador de contexto) e a classe MagicMock. Em Python, o mocking é feito principalmente com esses dois componentes poderosos.
O que NÃO queremos dizer com mocking?
Desenvolvedores usam muitos objetos ou módulos “mock”, que são substitutos locais totalmente funcionais para serviços de rede e APIs. Por exemplo, a biblioteca moto é uma biblioteca boto mockada que captura todas as chamadas de API boto e as processa localmente. Embora esses mocks permitam que desenvolvedores testem APIs externas localmente, ainda é preciso criar objetos reais. Esse não é o tipo de mocking abordado neste documento. Aqui, o foco é usar objetos MagicMock para controlar completamente o fluxo de execução da função em teste, facilitando o teste de falhas e do tratamento de exceções.
Como fazer mocking em Python?
Em Python, o mocking é feito usando patch para interceptar uma função de API ou uma chamada de criação de objeto. Quando patch intercepta uma chamada, ele retorna um objeto MagicMock por padrão. Ao definir propriedades no objeto MagicMock, você pode fazer com que a chamada de API retorne qualquer valor que quiser ou gere uma Exception.
O procedimento geral é o seguinte:
Escreva o teste como se estivesse usando APIs externas reais.
Na função em teste, identifique quais chamadas de API precisam ser substituídas por mocks; deve ser um número pequeno.
Na função de teste, aplique patch às chamadas de API.
Configure as respostas do objeto
MagicMock.Execute o teste.
Se o teste passar, pronto. Caso contrário, pode haver um erro na função em teste ou a resposta do MagicMock pode ter sido configurada incorretamente. A seguir, vamos conhecer melhor as ferramentas usadas para criar e configurar mocks.
patch
patch pode ser usado como decorador da função de teste, recebendo como argumento uma string com o nome da função que será substituída. Para que patch encontre a função, é preciso especificá-la pelo nome totalmente qualificado, que talvez não seja o que você espera. Se uma classe é importada com uma instrução from module import ClassA, ClassA passa a fazer parte do namespace do módulo para o qual foi importada.
Por exemplo, se uma classe for importada no módulo my_module.py da seguinte forma:
Ela deve receber patch como @patch(my_module.ClassA), e não como @patch(module.ClassA), devido à semântica da instrução from ... import ..., que importa classes e funções para o namespace atual.
Em geral, patch é usado para substituir uma chamada de API externa ou qualquer outra chamada de função ou criação de objeto que consuma muito tempo ou recursos. Você deve aplicar patch a apenas alguns elementos chamáveis por teste. Se perceber que está tentando usar patch mais do que algumas vezes, considere refatorar o teste ou a função que está testando.
O uso do decorador patch envia automaticamente um argumento posicional para a função decorada (ou seja, a função de teste). Quando várias funções recebem patch, o decorador mais próximo da função decorada é chamado primeiro e, por isso, cria o primeiro argumento posicional.
Por padrão, esses argumentos são instâncias de MagicMock, o objeto de mocking padrão de unittest.mock. Você pode definir o comportamento da função substituída configurando atributos na instância MagicMock retornada.
MagicMock
Os objetos MagicMock oferecem uma interface simples de mocking que permite definir o valor de retorno ou outro comportamento da função ou chamada de criação de objeto que recebeu patch. Assim, você pode definir completamente o comportamento da chamada e evitar a criação de objetos reais, que pode dar bastante trabalho. Por exemplo, se aplicarmos patch a uma chamada de requests.get, uma chamada de biblioteca HTTP, podemos definir a resposta que será retornada quando a função em teste fizer a chamada de API, em vez de garantir que um servidor de teste esteja disponível para retornar a resposta desejada.
Os dois atributos mais importantes de uma instância MagicMock são return_value e side_effect, que permitem definir o comportamento de retorno da chamada substituída.
return_value
O atributo return_value da instância MagicMock passada para a função de teste permite escolher o que o elemento chamável substituído vai retornar. Na maioria dos casos, você vai querer retornar uma versão mockada do que esse elemento retornaria normalmente. Pode ser JSON, um iterável, um valor, uma instância do objeto de resposta real, um MagicMock que simula o objeto de resposta ou praticamente qualquer outra coisa. Ao aplicar patch a objetos, a chamada substituída é a chamada de criação do objeto. Portanto, o return_value de MagicMock deve ser um objeto mockado, que pode ser outro MagicMock.
Se o código que você está testando segue as convenções do Python e usa duck typing em vez de tipagem explícita, pode ser conveniente usar um MagicMock como objeto de resposta. Em vez de se dar ao trabalho de criar uma instância real de uma classe, você pode definir pares arbitrários de chave e valor de atributos no construtor de MagicMock, e eles serão aplicados automaticamente à instância.
Observe que o argumento passado para test_some_func, ou seja, mock_api_call, é um MagicMock, e estamos definindo return_value como outro MagicMock. Em mocking, tudo é um MagicMock.
Definindo uma especificação para MagicMock
Embora a flexibilidade de um MagicMock seja conveniente para criar rapidamente mocks de classes com requisitos complexos, ela também pode ser uma desvantagem. Por padrão, os objetos MagicMock se comportam como se tivessem qualquer atributo, até mesmo atributos que você não quer que tenham. No exemplo acima, retornamos um objeto MagicMock em vez de um objeto Response. Mas imagine que cometemos um erro na chamada de patch e aplicamos patch a uma função que deveria retornar um objeto Request, em vez de um objeto Response. O MagicMock retornado ainda vai se comportar como se tivesse todos os atributos do objeto Request, embora a intenção fosse simular um objeto Response. Isso pode causar erros confusos nos testes e comportamentos incorretos.
A solução é definir uma spec para o MagicMock ao criá-lo, usando o argumento de palavra-chave spec: MagicMock(spec=Response). Isso cria um MagicMock que só permite acessar atributos e métodos da classe usada como especificação do MagicMock. Tentar acessar um atributo que não existe no objeto de origem gera um AttributeError, exatamente como aconteceria com o objeto real.
Um exemplo simples:
side_effect
Às vezes, você pode querer testar se sua função lida corretamente com uma exceção ou se várias chamadas da função substituída são tratadas corretamente. Para isso, use side_effect. Se você definir side_effect como uma exceção, ela será gerada imediatamente quando a função substituída for chamada.
Se você definir side_effect como um iterável, cada chamada da função substituída retornará o próximo item do iterável. Se definir side_effect como qualquer outro valor, esse valor será retornado.
assert_called_with
assert_called_with verifica se a função substituída foi chamada com os argumentos especificados em assert_called_with.
Um exemplo completo
Neste exemplo, estou testando uma função retry em Client.update. Isso significa que as chamadas de API em update serão feitas duas vezes — uma ótima oportunidade para usar MagicMock.side_effect.
O código completo do exemplo está aqui:
Estou aplicando patch a duas chamadas na função em teste (pyvars.vars_client.VarsClient.update): uma para VarsClient.get e outra para requests.post. Como estou aplicando patch a duas chamadas, minha função de teste recebe dois argumentos, que chamei de mock_post e mock_get. Ambos são objetos MagicMock. No estado padrão, eles não fazem muita coisa. Precisamos atribuir a eles alguns comportamentos de resposta.
Este teste verifica se o mecanismo de repetição funciona. Por isso, vou chamar update várias vezes e fazer várias chamadas a VarsClient.get e requests.post.
Aqui configuro os side_effect que quero. Quero que todas as chamadas a VarsClient.get funcionem (retornar um VarsResponse vazio serve para este teste), que a primeira chamada a requests.post falhe com uma exceção e que a segunda chamada a requests.post funcione. Esse tipo de controle detalhado do comportamento só é possível com mocking.
Depois de configurar os side_effects, o restante do teste é simples. O comportamento é o seguinte: a primeira chamada a requests.post falha, então o mecanismo de nova tentativa que envolve VarsClient.update deve capturar o erro, e tudo deve funcionar na segunda vez. Podemos verificar ainda melhor esse comportamento conferindo o histórico de chamadas de mock_get e mock_post.
Conclusão
Usar objetos simulados corretamente vai contra nossa intuição de tornar os testes o mais realistas e abrangentes possível. Ainda assim, isso nos permite escrever testes independentes, que são executados rapidamente e não têm dependências. Também nos dá a possibilidade de testar o tratamento de exceções e casos extremos que, de outra forma, seriam impossíveis de testar. Acima de tudo, temos liberdade para concentrar os testes na funcionalidade do nosso código, em vez de nos preocuparmos em configurar um ambiente de teste. Ao priorizar o que é importante testar, podemos aumentar a cobertura de testes e a confiabilidade do código — e é por isso que testamos.
Links da documentação
https://docs.python.org/3/library/unittest.mock.html
Segurança de IaC pensada para quem desenvolve
A Snyk protege sua infraestrutura como código do ciclo de vida do desenvolvimento de software até a execução na nuvem, com um mecanismo unificado de políticas como código para que todas as equipes possam desenvolver, implantar e operar com segurança.
