Declarando chamadas sucessivas para um método simulado


175

Mock tem um método útilassert_called_with() . No entanto, tanto quanto eu entendo isso apenas verifica a última chamada para um método.
Se eu tiver um código que chama o método simulado 3 vezes sucessivas, cada vez com parâmetros diferentes, como posso afirmar essas 3 chamadas com seus parâmetros específicos?

Respostas:


179

assert_has_calls é outra abordagem para esse problema.

Dos documentos:

assert_has_calls (chamadas, any_order = False)

afirmar que a simulação foi chamada com as chamadas especificadas. A lista mock_calls é verificada para as chamadas.

Se any_order for False (o padrão), as chamadas deverão ser seqüenciais. Pode haver chamadas extras antes ou depois das chamadas especificadas.

Se any_order for True, as chamadas poderão estar em qualquer ordem, mas todas deverão aparecer em mock_calls.

Exemplo:

>>> from unittest.mock import call, Mock
>>> mock = Mock(return_value=None)
>>> mock(1)
>>> mock(2)
>>> mock(3)
>>> mock(4)
>>> calls = [call(2), call(3)]
>>> mock.assert_has_calls(calls)
>>> calls = [call(4), call(2), call(3)]
>>> mock.assert_has_calls(calls, any_order=True)

Fonte: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls


9
Um pouco estranho que eles escolheram para adicionar um novo tipo de "apelo" para que eles também poderia ter usado apenas uma lista ou uma tupla ...
jaapz

@jaapz Subclasses tuple: isinstance(mock.call(1), tuple)True. Eles também adicionaram alguns métodos e atributos.
Jpmc26

13
As primeiras versões do Mock usavam uma tupla simples, mas é difícil de usar. Cada chamada de função recebe uma tupla de (args, kwargs); portanto, para verificar se "foo (123)" foi chamado corretamente, é necessário "afirmar mock.call_args == ((123,), {})", que é um bocado comparado ao "call (123)"
Jonathan Hartley

O que você faz quando, em cada instância da chamada, espera um valor de retorno diferente?
CodeWithPride

2
@CodeWithPride parece mais um trabalho paraside_effect
Pigueiras 08/08

108

Normalmente, eu não me importo com a ordem das ligações, apenas com o que elas aconteceram. Nesse caso, eu combino assert_any_callcom uma afirmação sobre call_count.

>>> import mock
>>> m = mock.Mock()
>>> m(1)
<Mock name='mock()' id='37578160'>
>>> m(2)
<Mock name='mock()' id='37578160'>
>>> m(3)
<Mock name='mock()' id='37578160'>
>>> m.assert_any_call(1)
>>> m.assert_any_call(2)
>>> m.assert_any_call(3)
>>> assert 3 == m.call_count
>>> m.assert_any_call(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "[python path]\lib\site-packages\mock.py", line 891, in assert_any_call
    '%s call not found' % expected_string
AssertionError: mock(4) call not found

Acho que fazer dessa maneira é mais fácil de ler e entender do que uma grande lista de chamadas passadas em um único método.

Se você se importa com o pedido ou espera várias chamadas idênticas, assert_has_callspode ser mais apropriado.

Editar

Desde que publiquei esta resposta, repensei minha abordagem aos testes em geral. Eu acho que vale a pena mencionar que, se o seu teste está ficando complicado, você pode estar testando inadequadamente ou ter um problema de design. As zombarias são projetadas para testar a comunicação entre objetos em um design orientado a objetos. Se o seu design não for orientado a objeções (como em mais processuais ou funcionais), a simulação pode ser totalmente inadequada. Você também pode ter muita coisa acontecendo dentro do método ou pode estar testando detalhes internos que devem ser deixados sem zerar. Desenvolvi a estratégia mencionada neste método quando meu código não era muito orientado a objetos e acredito que também estava testando detalhes internos que seria melhor deixar de serem desmembrados.


@ jpmc26 você poderia elaborar mais sobre sua edição? O que você quer dizer com "melhor deixado desmembrado"? Quanto mais você testar se uma chamada foi feita dentro de um método
otgw

@memo Muitas vezes, é melhor deixar o método real ser chamado. Se o outro método for quebrado, ele pode interromper o teste, mas o valor de evitar isso é menor que o valor de um teste mais simples e mais sustentável. Os melhores momentos para zombar são quando a chamada externa para o outro método é o que você deseja testar (geralmente, isso significa que algum tipo de resultado é passado para ele e o código sob teste não retorna um resultado.) Ou o outro método possui dependências externas (banco de dados, sites) que você deseja eliminar. (Tecnicamente, o último caso é mais um stub, e eu hesitaria em afirmar sobre ele.)
jpmc26

A zombaria @ jpmc26 é útil quando você deseja evitar a injeção de dependência ou algum outro método de escolha da estratégia de tempo de execução. como você mencionou, testar a lógica interna dos métodos, sem chamar serviços externos e, mais importante, sem ter consciência do ambiente (não, não é um bom código do() if TEST_ENV=='prod' else dont()), é conseguido facilmente zombando da maneira sugerida. um efeito colateral disso é ser para manter testes por versões (alterações de código digamos entre google search v1 API e v2, seu código irá testar a versão 1 não importa qual)
Daniel Dubovski

@DanielDubovski A maioria dos seus testes deve ser baseada em entrada / saída. Isso nem sempre é possível, mas se não for possível na maioria das vezes, você provavelmente terá um problema de design. Quando você precisa de algum valor retornado que normalmente vem de outro trecho de código e deseja cortar uma dependência, normalmente um stub funciona. As zombarias são necessárias apenas quando você precisa verificar se alguma função de modificação de estado (provavelmente sem valor de retorno) é chamada. (A diferença entre um mock e um stub é que você não faz uma ligação com um stub.) Usar zombarias onde os stubs farão faz com que seus testes sejam menos sustentáveis.
jpmc26

@ jpmc26 não está chamando um serviço externo de um tipo de saída? é claro que você pode refatorar o código que cria a mensagem a ser enviada e testá-la em vez de afirmar os parâmetros de chamada, mas IMHO, é praticamente o mesmo. Como você sugeriria redesenhar a chamada de APIs externas? Concordo que a zombaria deve ser reduzida ao mínimo, tudo o que estou dizendo é que você não pode testar os dados enviados a serviços externos para garantir que a lógica esteja se comportando conforme o esperado.
Daniel Dubovski


17

Eu sempre tenho que procurar isso uma e outra vez, então aqui está a minha resposta.


Afirmando várias chamadas de método em diferentes objetos da mesma classe

Suponha que tenhamos uma classe de serviço pesado (da qual queremos zombar):

In [1]: class HeavyDuty(object):
   ...:     def __init__(self):
   ...:         import time
   ...:         time.sleep(2)  # <- Spends a lot of time here
   ...:     
   ...:     def do_work(self, arg1, arg2):
   ...:         print("Called with %r and %r" % (arg1, arg2))
   ...:  

Aqui está um código que usa duas instâncias da HeavyDutyclasse:

In [2]: def heavy_work():
   ...:     hd1 = HeavyDuty()
   ...:     hd1.do_work(13, 17)
   ...:     hd2 = HeavyDuty()
   ...:     hd2.do_work(23, 29)
   ...:    


Agora, aqui está um caso de teste para a heavy_workfunção:

In [3]: from unittest.mock import patch, call
   ...: def test_heavy_work():
   ...:     expected_calls = [call.do_work(13, 17),call.do_work(23, 29)]
   ...:     
   ...:     with patch('__main__.HeavyDuty') as MockHeavyDuty:
   ...:         heavy_work()
   ...:         MockHeavyDuty.return_value.assert_has_calls(expected_calls)
   ...:  

Estamos zombando da HeavyDutyclasse MockHeavyDuty. Para declarar chamadas de método provenientes de todas as HeavyDutyinstâncias às quais temos que nos referir MockHeavyDuty.return_value.assert_has_calls, em vez de MockHeavyDuty.assert_has_calls. Além disso, na lista de expected_callsnós temos que especificar qual nome de método estamos interessados ​​em afirmar chamadas. Portanto, nossa lista é feita de chamadas para call.do_work, em vez de simplesmente call.

O exercício do caso de teste mostra que é bem-sucedido:

In [4]: print(test_heavy_work())
None


Se modificarmos a heavy_workfunção, o teste falhará e produzirá uma mensagem de erro útil:

In [5]: def heavy_work():
   ...:     hd1 = HeavyDuty()
   ...:     hd1.do_work(113, 117)  # <- call args are different
   ...:     hd2 = HeavyDuty()
   ...:     hd2.do_work(123, 129)  # <- call args are different
   ...:     

In [6]: print(test_heavy_work())
---------------------------------------------------------------------------
(traceback omitted for clarity)

AssertionError: Calls not found.
Expected: [call.do_work(13, 17), call.do_work(23, 29)]
Actual: [call.do_work(113, 117), call.do_work(123, 129)]


Declarando várias chamadas para uma função

Para contrastar com o acima, aqui está um exemplo que mostra como simular várias chamadas para uma função:

In [7]: def work_function(arg1, arg2):
   ...:     print("Called with args %r and %r" % (arg1, arg2))

In [8]: from unittest.mock import patch, call
   ...: def test_work_function():
   ...:     expected_calls = [call(13, 17), call(23, 29)]    
   ...:     with patch('__main__.work_function') as mock_work_function:
   ...:         work_function(13, 17)
   ...:         work_function(23, 29)
   ...:         mock_work_function.assert_has_calls(expected_calls)
   ...:    

In [9]: print(test_work_function())
None


Existem duas diferenças principais. A primeira é que, ao zombar de uma função, configuramos nossas chamadas esperadas usando call, em vez de usar call.some_method. A segunda é que recorremos , assert_has_callsem mock_work_functionvez de continuar mock_work_function.return_value.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.