Asserire chiamate successive a un metodo simulato


175

Il finto ha un metodo utileassert_called_with() . Tuttavia, per quanto ho capito, controlla solo l' ultima chiamata a un metodo.
Se ho un codice che chiama il metodo deriso 3 volte in successione, ogni volta con parametri diversi, come posso affermare queste 3 chiamate con i loro parametri specifici?

Risposte:


179

assert_has_calls è un altro approccio a questo problema.

Dai documenti:

assert_has_calls (Calls, any_order = False)

asserire che il mock è stato chiamato con le chiamate specificate. L'elenco mock_calls è controllato per le chiamate.

Se any_order è False (impostazione predefinita), le chiamate devono essere sequenziali. Possono esserci chiamate extra prima o dopo le chiamate specificate.

Se any_order è True, le chiamate possono essere in qualsiasi ordine, ma devono apparire tutte in mock_calls.

Esempio:

>>> 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
Un po 'strano hanno scelto di aggiungere un nuovo tipo di "chiamata" per il quale avrebbero anche potuto usare un elenco o una tupla ...
Jaapz,

@jaapz Sottoclassi tuple: isinstance(mock.call(1), tuple)True. Hanno anche aggiunto alcuni metodi e attributi.
jpmc26,

13
Le prime versioni di Mock usavano una semplice tupla, ma risulta essere scomodo da usare. Ogni chiamata di funzione riceve una tupla di (args, kwargs), quindi per verificare che "foo (123)" sia stato chiamato correttamente, devi "asserire mock.call_args == ((123,), {})", che è un boccone rispetto a "call (123)"
Jonathan Hartley

Cosa fai quando su ogni istanza di chiamata ti aspetti un valore di ritorno diverso?
CodeWith Pride

2
@CodeWithPride sembra più un lavoro perside_effect
Pigueiras

108

Di solito, non mi interessa l'ordine delle chiamate, solo che sono successe. In tal caso, mi associo assert_any_calla un'affermazione in merito 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

Trovo che sia così più facile da leggere e comprendere rispetto a un ampio elenco di chiamate passate in un unico metodo.

Se ti interessa l'ordine o ti aspetti più chiamate identiche, assert_has_callspotrebbe essere più appropriato.

modificare

Da quando ho pubblicato questa risposta, ho ripensato il mio approccio ai test in generale. Penso che valga la pena ricordare che se il tuo test sta diventando così complicato, potresti provare in modo inappropriato o avere un problema di progettazione. I mock sono progettati per testare la comunicazione tra oggetti in un design orientato agli oggetti. Se il tuo design non è orientato agli oggetti (come in più procedurali o funzionali), la simulazione potrebbe essere totalmente inappropriata. È possibile che nel metodo si verifichino troppe cose oppure si potrebbero verificare i dettagli interni che è meglio lasciare intatti. Ho sviluppato la strategia menzionata in questo metodo quando il mio codice non era molto orientato agli oggetti, e credo che stavo anche testando i dettagli interni che sarebbe stato meglio lasciare sballati.


@ jpmc26 potresti approfondire la tua modifica? Cosa intendi con "meglio lasciato libero"? In quale altro modo
testereste

@memo Spesso, è meglio lasciare che venga chiamato il metodo reale. Se l'altro metodo viene interrotto, potrebbe interrompere il test, ma il valore di evitarlo è inferiore al valore di avere un test più semplice e più gestibile. I periodi migliori per deridere sono quando la chiamata esterna all'altro metodo è ciò che si desidera verificare (di solito, ciò significa che viene passato un tipo di risultato e il codice in prova non restituisce un risultato.) O l'altro metodo ha dipendenze esterne (database, siti Web) che si desidera eliminare. (Tecnicamente, l'ultimo caso è più di uno stub, e non esiterei ad affermarlo.)
jpmc26,

@ jpmc26 deridere è utile quando si desidera evitare l'iniezione di dipendenza o qualche altro metodo di scelta della strategia di runtime. come hai menzionato, testare la logica interna dei metodi, senza chiamare servizi esterni e, cosa più importante, senza essere consapevoli dell'ambiente (un no no per avere un buon codice do() if TEST_ENV=='prod' else dont()), si ottiene facilmente deridendo il modo in cui hai suggerito. un effetto collaterale di questo è quello di mantenere i test per versione (diciamo che le modifiche al codice tra ricerca di api v1 e v2 di google, il tuo codice testerà la versione 1 non importa quale))
Daniel Dubovski

@DanielDubovski La maggior parte dei test dovrebbe essere basata su input / output. Questo non è sempre possibile, ma se non è possibile per la maggior parte del tempo, probabilmente hai un problema di progettazione. Quando hai bisogno di un valore restituito che normalmente proviene da un altro pezzo di codice e vuoi tagliare una dipendenza, di solito lo farà uno stub. Le simulazioni sono necessarie solo quando è necessario verificare che venga chiamata una funzione di modifica dello stato (probabilmente senza valore di ritorno). (La differenza tra un mock e uno stub è che non si fa valere una chiamata con uno stub.) L'uso di mock in cui gli stub faranno rende i test meno gestibili.
jpmc26,

@ jpmc26 non sta chiamando un servizio esterno una sorta di output? ovviamente puoi refactificare il codice che costruisce il messaggio da inviare e testarlo invece di affermare i parametri di chiamata, ma IMHO, è praticamente lo stesso. Come suggeriresti di ridisegnare la chiamata di API esterne? Sono d'accordo che il derisione dovrebbe essere ridotto al minimo, tutto ciò che sto dicendo è che non puoi andare in giro a testare i dati che invii a servizi esterni per assicurarti che la logica si comporti come previsto.
Daniel Dubovski,


17

Devo sempre cercare questo uno e più volte, quindi ecco la mia risposta.


L'asserzione di più chiamate di metodo su oggetti diversi della stessa classe

Supponiamo di avere una classe heavy duty (che vogliamo deridere):

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))
   ...:  

ecco del codice che utilizza due istanze della HeavyDutyclasse:

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


Ora, ecco un caso di test per la heavy_workfunzione:

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)
   ...:  

Stiamo deridendo la HeavyDutyclasse con MockHeavyDuty. Per affermare le chiamate di metodo provenienti da ogni HeavyDutyistanza a cui dobbiamo fare riferimento MockHeavyDuty.return_value.assert_has_calls, anziché MockHeavyDuty.assert_has_calls. Inoltre, nell'elenco di expected_callsdobbiamo specificare quale nome del metodo siamo interessati ad affermare le chiamate. Quindi il nostro elenco è composto da chiamate call.do_work, anziché semplicemente call.

L'esercizio del test case ci mostra che ha successo:

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


Se modifichiamo la heavy_workfunzione, il test fallisce e genera un utile messaggio di errore:

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)]


Asserire più chiamate a una funzione

In contrasto con quanto sopra, ecco un esempio che mostra come deridere più chiamate a una funzione:

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


Ci sono due differenze principali. Il primo è che quando deridiamo una funzione impostiamo le nostre chiamate previste usando call, invece di usare call.some_method. La seconda è che noi chiamiamo assert_has_callssu mock_work_function, invece che su mock_work_function.return_value.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.