Python mockuje wiele zwracanych wartości

168

Używam Pythona mock.patch i chciałbym zmienić wartość zwracaną dla każdego połączenia. Oto zastrzeżenie: łatana funkcja nie ma żadnych danych wejściowych, więc nie mogę zmienić wartości zwracanej na podstawie danych wejściowych.

Oto mój kod w celach informacyjnych.

def get_boolean_response():
    response = io.prompt('y/n').lower()
    while response not in ('y', 'n', 'yes', 'no'):
        io.echo('Not a valid input. Try again'])
        response = io.prompt('y/n').lower()

    return response in ('y', 'yes')

Mój kod testowy:

@mock.patch('io')
def test_get_boolean_response(self, mock_io):
    #setup
    mock_io.prompt.return_value = ['x','y']
    result = operations.get_boolean_response()

    #test
    self.assertTrue(result)
    self.assertEqual(mock_io.prompt.call_count, 2)

io.promptjest po prostu niezależną od platformy (python 2 i 3) wersją "input". Ostatecznie więc staram się wyśmiewać dane wejściowe użytkowników. Próbowałem użyć listy dla wartości zwracanej, ale wydaje się, że to nie działa.

Możesz zobaczyć, że jeśli wartość zwracana jest czymś nieprawidłowym, po prostu otrzymam tutaj nieskończoną pętlę. Potrzebuję więc sposobu, aby ostatecznie zmienić wartość zwracaną, aby mój test faktycznie się zakończył.

(innym możliwym sposobem odpowiedzi na to pytanie może być wyjaśnienie, w jaki sposób mogę naśladować dane wejściowe użytkownika w teście jednostkowym)


Nie jest dupkiem w tym pytaniu, głównie dlatego, że nie mam możliwości zmiany danych wejściowych.

Jeden z komentarzy w odpowiedzi na to pytanie jest podobny, ale nie podano odpowiedzi / komentarza.

Nick Humrich
źródło
3
response is not 'y' or 'n' or 'yes' or 'no'w nie robić to, co myślisz, że to robi. Zobacz Jak przetestować jedną zmienną pod kątem wielu wartości? i należy nie używać isdo porównania wartości ciągów, korzystanie ==porównać wartości , a nie tożsamości obiektów.
Martijn Pieters
Uważaj też tutaj. Wygląda na to, że próbujesz użyć isdo porównania literałów ciągów. Nie rób tego. Fakt, że to działa (czasami) jest tylko szczegółem implementacji w CPythonie. Poza tym response is not 'y' or 'n' or 'yes' or 'no'prawdopodobnie nie robi tego, co myślisz, że jest ...
mgilson

Odpowiedzi:

300

Możesz przypisać iterowalne do side_effect, a makieta zwróci następną wartość w sekwencji za każdym razem, gdy zostanie wywołana:

>>> from unittest.mock import Mock
>>> m = Mock()
>>> m.side_effect = ['foo', 'bar', 'baz']
>>> m()
'foo'
>>> m()
'bar'
>>> m()
'baz'

Cytując Mock()dokumentację :

Jeśli side_effect jest iterowalny, to każde wywołanie makiety zwróci następną wartość z iterowalnego.

Tak na marginesie, test response is not 'y' or 'n' or 'yes' or 'no'będzie nie działa; pytasz, czy wyrażenie (response is not 'y')jest prawdziwe, czy 'y'prawdziwe (zawsze tak jest, niepusty łańcuch jest zawsze prawdziwy) itd. Różne wyrażenia po obu stronach oroperatorów są niezależne . Zobacz Jak przetestować jedną zmienną pod kątem wielu wartości?

Nie należy również używać isdo testowania na łańcuchu. Interpreter CPythona może w pewnych okolicznościach ponownie używać obiektów łańcuchowych , ale nie jest to zachowanie, na które należy liczyć.

W związku z tym użyj:

response not in ('y', 'n', 'yes', 'no')

zamiast; użyje to testów równości ( ==) do określenia, czy responseodwołuje się do łańcucha o tej samej zawartości (wartości).

To samo dotyczy response == 'y' or 'yes'; użyj response in ('y', 'yes')zamiast tego.

Martijn Pieters
źródło
Czy można to zrobić za pomocą standardu mock? Czy istnieje sposób na użycie łatki z MagicMock, tak jak robię to ze standardowym mockiem?
Nick Humrich
@Humdinger: To jest cecha Mockklasy stardard .
Martijn Pieters
17
Wydaje się, że przypisanie listy działa tylko z Pythonem 3. Testowanie w Pythonie 2.7 Zamiast tego potrzebuję użyć iteratora ( m.side_effect = iter(['foo', 'bar', 'baz'])).
user686249
1
@ user686249: Rzeczywiście mogę to odtworzyć, ponieważ specyfikacja z metody daje lambda(funkcję), a nie MagicMock. Obiekt funkcji nie może mieć właściwości, więc side_effectatrybut musi być iterowalny. Nie powinieneś jednak określać metody w ten sposób. Lepsze wykorzystanie mock.patch.object(requests.Session, 'post'); skutkuje to obiektem patchera, który poprawnie automatycznie określa metodę i obsługuje ją side_effectpoprawnie.
Martijn Pieters
3
@ JoeMjr2: Kiedy iterator jest wyczerpany, StopIterationpodnosi się. Możesz użyć dowolnego iteratora, więc możesz użyć itertools.chain(['Foo'], itertools.repeat('Bar'))do wyprodukowania Fooraz, a potem na zawsze Bar.
Martijn Pieters