Jak wyśmiewać funkcję open używaną w instrukcji with (używając frameworku Mock w Pythonie)?


188

Jak przetestować następujący kod za pomocą próbnych (używając próbnych, dekoratora łatek i wartowników dostarczonych przez frameworka Michaela Foorda ):

def testme(filepath):
    with open(filepath, 'r') as f:
        return f.read()

@Daryl Spitzer: czy mógłbyś pominąć meta-pytanie („znam odpowiedź ...”) To mylące.
S.Lott,

W przeszłości, kiedy to zostawiłem, ludzie narzekali, że odpowiadam na własne pytanie. Spróbuję przenieść to do mojej odpowiedzi.
Daryl Spitzer,

1
@Daryl: Najlepszym sposobem na uniknięcie skarg na odpowiedź na własne pytanie, które zwykle wynikają z obaw o „dziwki karmy”, jest oznaczenie pytania i / lub odpowiedzi jako „wiki społeczności”.
John Millikin,

3
Jeśli odpowiedź na twoje pytanie jest uważana za Whoring Karma, myślę, że FAQ powinno być wyjaśnione w tej kwestii.
EBGreen,

Odpowiedzi:


132

Sposób na dokonanie tego zmienił się w pozornej wersji 0.7.0, która wreszcie obsługuje wyśmiewanie metod protokołu python (metody magiczne), szczególnie przy użyciu MagicMock:

http://www.voidspace.org.uk/python/mock/magicmock.html

Przykład wyśmiewania otwarty jako menedżer kontekstu (ze strony przykładów w dokumentacji próbnej):

>>> open_name = '%s.open' % __name__
>>> with patch(open_name, create=True) as mock_open:
...     mock_open.return_value = MagicMock(spec=file)
...
...     with open('/some/path', 'w') as f:
...         f.write('something')
...
<mock.Mock object at 0x...>
>>> file_handle = mock_open.return_value.__enter__.return_value
>>> file_handle.write.assert_called_with('something')

Łał! Wygląda to na znacznie prostsze niż przykład menedżera kontekstu znajdujący się obecnie na stronie voidspace.org.uk/python/mock/magicmock.html, który wyraźnie ustawia __enter__i __exit__wyśmiewa również obiekty - czy to drugie podejście jest nieaktualne, czy nadal przydatne?
Brandon Rhodes

6
„Drugie podejście” pokazuje, jak to zrobić bez korzystania z MagicMock (tj. Jest to tylko przykład tego, jak Mock obsługuje metody magiczne). Jeśli używasz MagicMock (jak wyżej), a następnie wejść i wyjście są wstępnie skonfigurowane dla Ciebie.
fuzzyman

5
możesz wskazać swój post na blogu, w którym wyjaśnisz bardziej szczegółowo, dlaczego / jak to działa
Rodrigue

9
W Pythonie 3 „plik” nie jest zdefiniowany (używany w specyfikacji MagicMock), więc zamiast tego używam io.IOBase.
Jonathan Hartley

1
Uwaga: w Pythonie 3 wbudowane filenie ma!
ekshuma

239

mock_openjest częścią mockframeworka i jest bardzo prosty w użyciu. patchużyte jako kontekst zwraca obiekt użyty do zastąpienia załatanego: możesz go użyć, aby uprościć test.

Python 3.x

Użyj builtinszamiast __builtin__.

from unittest.mock import patch, mock_open
with patch("builtins.open", mock_open(read_data="data")) as mock_file:
    assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")

Python 2.7

mocknie jest częścią unittesti powinieneś łatać__builtin__

from mock import patch, mock_open
with patch("__builtin__.open", mock_open(read_data="data")) as mock_file:
    assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")

Skrzynia dekoratora

Jeśli użyjesz patchjako dekoratora, używając mock_open()wyniku jako new patchargumentu, może to być trochę dziwne.

W takim przypadku lepiej użyć new_callable patchargumentu i pamiętać, że każdy dodatkowy argument, patchktóry nie będzie używany, zostanie przekazany do new_callabledziałania zgodnie z opisem w patchdokumentacji .

patch () przyjmuje dowolne argumenty słów kluczowych. Zostaną one przekazane Mock (lub new_callable) podczas budowy.

Na przykład udekorowana wersja dla Python 3.x to:

@patch("builtins.open", new_callable=mock_open, read_data="data")
def test_patch(mock_file):
    assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")

Pamiętaj, że w tym przypadku patchdoda obiekt próbny jako argument funkcji testowej.


Przepraszam, że pytasz, czy with patch("builtins.open", mock_open(read_data="data")) as mock_file:można przekonwertować na składnię dekoratora? Próbowałem, ale nie jestem pewien, na co muszę przejść @patch("builtins.open", ...) jako drugi argument.
imrek

1
@DrunkenMaster Updateted .. dzięki za wskazane. Korzystanie z dekoratora nie jest w tym przypadku trywialne.
Michele d'Amico

Grazie! Mój problem był nieco bardziej złożony (musiałem przekierować return_valueof mock_openna inny próbny obiekt i potwierdzić drugie próbne return_value), ale zadziałało, dodając mock_openas new_callable.
imrek

1
@ArthurZopellaro spójrz na sixmoduł, aby mieć spójny mockmoduł. Ale nie wiem, czy mapuje również builtinswe wspólnym module.
Michele d'Amico

1
Jak znaleźć poprawną nazwę do łatania? Tj. Jak znaleźć pierwszy argument @patch (w tym przypadku „buildins.open”) dla dowolnej funkcji?
zenperttu,

73

Dzięki najnowszym wersjom mocka możesz użyć naprawdę przydatnego pomocnika mock_open :

mock_open (mock = None, read_data = None)

Funkcja pomocnika do stworzenia makiety, która zastąpi użycie open. Działa dla open wywoływany bezpośrednio lub używany jako menedżer kontekstu.

Argument próbny to próbny obiekt do skonfigurowania. Jeśli Brak (domyślnie), zostanie utworzony MagicMock, z API ograniczonym do metod lub atrybutów dostępnych w standardowych uchwytach plików.

read_data to ciąg znaków zwracany przez metodę read uchwytu pliku. Domyślnie jest to pusty ciąg.

>>> from mock import mock_open, patch
>>> m = mock_open()
>>> with patch('{}.open'.format(__name__), m, create=True):
...    with open('foo', 'w') as h:
...        h.write('some stuff')

>>> m.assert_called_once_with('foo', 'w')
>>> handle = m()
>>> handle.write.assert_called_once_with('some stuff')

Jak sprawdzić, czy jest wiele .writepołączeń?
n611x007,

1
@naxa Jednym ze sposobów jest przekazanie każdego oczekiwanego parametru do handle.write.assert_any_call(). Możesz także użyć, handle.write.call_args_listaby odebrać każde połączenie, jeśli kolejność jest ważna.
Rob Cutmore,

m.return_value.write.assert_called_once_with('some stuff')jest lepsze imo. Unika rejestracji połączenia.
Anonimowy

2
Ręczne zapewnianie Mock.call_args_listjest bezpieczniejsze niż wywoływanie którejkolwiek z Mock.assert_xxxmetod. Jeśli źle przeliterujesz którykolwiek z tych drugich, będąc atrybutami Mocka, zawsze będą cicho przechodzić.
Jonathan Hartley,

12

Aby użyć mock_open dla prostego pliku read()(oryginalny fragment mock_open już podany na tej stronie jest przeznaczony bardziej do zapisu):

my_text = "some text to return when read() is called on the file object"
mocked_open_function = mock.mock_open(read_data=my_text)

with mock.patch("__builtin__.open", mocked_open_function):
    with open("any_string") as f:
        print f.read()

Uwaga, jak w dokumentacji dla mock_open, jest to specjalnie dla read() , więc nie będzie działać z typowymi wzorcami, takimi jak for line in fna przykład.

Używa języka Python 2.6.6 / mock 1.0.1


Wygląda dobrze, ale nie mogę zmusić go do pracy z for line in opened_file:rodzajem kodu. Próbowałem eksperymentować z iterowalnym StringIO, które implementuje __iter__i używa tego zamiast my_text, ale bez powodzenia.
Evgen,

@EvgeniiPuchkaryov Działa to specjalnie, read()więc nie będzie działać w twoim for line in opened_fileprzypadku; Zredagowałem post, aby wyjaśnić
jlb83

1
Obsługę @EvgeniiPuchkaryov for line in f:można osiągnąć przez wyśmiewanie wartości zwracanej open()jako obiektu StringIO .
Iskar Jarak,

1
Aby wyjaśnić, testowany system (SUT) w tym przykładzie to: with open("any_string") as f: print f.read()
Brad M

4

Najlepsza odpowiedź jest przydatna, ale trochę ją rozwinąłem.

Jeśli chcesz ustawić wartość swojego obiektu pliku ( fin as f) na podstawie argumentów przekazanych open()tutaj, możesz to zrobić w następujący sposób:

def save_arg_return_data(*args, **kwargs):
    mm = MagicMock(spec=file)
    mm.__enter__.return_value = do_something_with_data(*args, **kwargs)
    return mm
m = MagicMock()
m.side_effect = save_arg_return_array_of_data

# if your open() call is in the file mymodule.animals 
# use mymodule.animals as name_of_called_file
open_name = '%s.open' % name_of_called_file

with patch(open_name, m, create=True):
    #do testing here

Zasadniczo open()zwróci obiekt i withwywoła __enter__()ten obiekt.

Aby poprawnie kpić, musimy kpić, open()aby zwrócić próbny obiekt. Ten próbny obiekt powinien następnie wyśmiewać __enter__()wywołanie ( MagicMockzrobi to za nas), aby zwrócić próbny obiekt danych / pliku, który chcemy (stąd mm.__enter__.return_value). Wykonanie tego przy użyciu 2 próbnych sposobów w powyższy sposób pozwala nam uchwycić przekazane argumenty open()i przekazać je naszymdo_something_with_data metodzie.

Przekazałem cały próbny plik jako ciąg znaków open()i do_something_with_datawyglądałem tak:

def do_something_with_data(*args, **kwargs):
    return args[0].split("\n")

To przekształca ciąg w listę, dzięki czemu możesz wykonać następujące czynności, tak jak w przypadku normalnego pliku:

for line in file:
    #do action

Jeśli testowany kod obsługuje plik w inny sposób, na przykład przez wywołanie jego funkcji „readline”, możesz zwrócić dowolny próbny obiekt w funkcji „do_something_with_data” z pożądanymi atrybutami.
user3289695,

Czy istnieje sposób na uniknięcie dotykania __enter__? To zdecydowanie bardziej przypomina hack niż zalecany sposób.
imrek

Enter to sposób pisania menedżerów kontekstów, takich jak open (). Drwiny często są nieco hackerskie, ponieważ musisz uzyskać dostęp do „prywatnych” rzeczy, aby wyśmiewać, ale wejście tutaj nie jest z natury hacky imo
theannouncer

3

Mogę być trochę spóźniony do gry, ale zadziałało to dla mnie, gdy zadzwoniłem opendo innego modułu bez konieczności tworzenia nowego pliku.

test.py

import unittest
from mock import Mock, patch, mock_open
from MyObj import MyObj

class TestObj(unittest.TestCase):
    open_ = mock_open()
    with patch.object(__builtin__, "open", open_):
        ref = MyObj()
        ref.save("myfile.txt")
    assert open_.call_args_list == [call("myfile.txt", "wb")]

MyObj.py

class MyObj(object):
    def save(self, filename):
        with open(filename, "wb") as f:
            f.write("sample text")

Przez łatanie openfunkcji w __builtin__moim module mock_open()mogę wyśmiewać zapisywanie do pliku bez jego tworzenia.

Uwaga: Jeśli korzystasz z modułu, który używa cytonu, lub twój program w jakikolwiek sposób zależy od cytonu, musisz zaimportować moduł cytonu, umieszczając__builtin__ go import __builtin__na górze pliku. Jeśli __builtin__używasz cytonu, nie będziesz mógł kpić z tego uniwersalnego .


Odmiana tego podejścia działała dla mnie, ponieważ większość testowanego kodu znajdowała się w innych modułach, jak pokazano tutaj. Musiałem koniecznie dodać import __builtin__do mojego modułu testowego. Ten artykuł pomógł wyjaśnić, dlaczego ta technika działa tak dobrze, jak ona: ichimonji10.name/blog/6
killthrush

0

Aby załatać wbudowaną funkcję open () z unittest:

Działa to w przypadku poprawki do odczytu konfiguracji json.

class ObjectUnderTest:
    def __init__(self, filename: str):
        with open(filename, 'r') as f:
            dict_content = json.load(f)

Wyśmiewanym obiektem jest obiekt io.TextIOWrapper zwrócony przez funkcję open ()

@patch("<src.where.object.is.used>.open",
        return_value=io.TextIOWrapper(io.BufferedReader(io.BytesIO(b'{"test_key": "test_value"}'))))
    def test_object_function_under_test(self, mocker):

0

Jeśli nie potrzebujesz już żadnego pliku, możesz udekorować metodę testową:

@patch('builtins.open', mock_open(read_data="data"))
def test_testme():
    result = testeme()
    assert result == "data"
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.