Wie verspotte ich ein Open, das in einer with-Anweisung verwendet wird (mithilfe des Mock-Frameworks in Python)?

188

Wie teste ich den folgenden Code mit Mocks (unter Verwendung von Mocks, dem Patch-Dekorator und Sentinels, die vom Mock-Framework von Michael Foord bereitgestellt werden ):

def testme(filepath):
    with open(filepath, 'r') as f:
        return f.read()
Daryl Spitzer
quelle
@Daryl Spitzer: Könntest du die Meta-Frage weglassen ("Ich kenne die Antwort ..."). Es ist verwirrend.
S.Lott
In der Vergangenheit, als ich aufgehört habe, haben sich die Leute beschwert, dass ich meine eigene Frage beantworte. Ich werde versuchen, das auf meine Antwort zu verschieben.
Daryl Spitzer
1
@Daryl: Der beste Weg, um Beschwerden über die Beantwortung der eigenen Frage zu vermeiden, die normalerweise aus der Sorge um "Karma-Huren" resultieren, besteht darin, die Frage und / oder Antwort als "Community-Wiki" zu markieren.
John Millikin
3
Wenn die Beantwortung Ihrer eigenen Frage als Karma-Hure betrachtet wird, sollten die häufig gestellten Fragen zu diesem Punkt geklärt werden, denke ich.
EBGreen

Antworten:

131

Die Vorgehensweise hat sich in Mock 0.7.0 geändert, das schließlich das Verspotten der Python-Protokollmethoden (Magic-Methoden) unterstützt, insbesondere mit MagicMock:

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

Ein Beispiel für das Verspotten als Kontextmanager (auf der Beispielseite in der Verspottungsdokumentation):

>>> 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')
Fuzzyman
quelle
Beeindruckend! Dies sieht viel einfacher aus als das Beispiel für den Kontextmanager unter voidspace.org.uk/python/mock/magicmock.html, in dem Objekte explizit festgelegt __enter__und __exit__verspottet werden. Ist der letztere Ansatz veraltet oder immer noch nützlich?
Brandon Rhodes
5
Der "letztere Ansatz" zeigt, wie man es ohne Verwendung eines MagicMock macht (dh es ist nur ein Beispiel dafür, wie Mock magische Methoden unterstützt). Wenn Sie ein MagicMock (wie oben) verwenden, sind die Ein- und Ausgänge für Sie vorkonfiguriert.
Fuzzyman
5
Sie könnten auf Ihren Blog-Beitrag verweisen, in dem Sie ausführlicher erklären, warum / wie das funktioniert
Rodrigue
9
In Python 3 ist 'Datei' nicht definiert (wird in der MagicMock-Spezifikation verwendet), daher verwende ich stattdessen io.IOBase.
Jonathan Hartley
1
Hinweis: In Python3 ist das eingebaute fileweg!
Exhuma
239

mock_openist Teil des mockFrameworks und sehr einfach zu bedienen. patchAls Kontext verwendet gibt das Objekt zurück, das zum Ersetzen des gepatchten Objekts verwendet wird: Sie können es verwenden, um Ihren Test zu vereinfachen.

Python 3.x.

Verwenden Sie builtinsanstelle von __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

mockist nicht Teil von unittestund Sie sollten patchen__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")

Dekorateur Fall

Wenn Sie patchals Dekorateur mock_open()das Ergebnis von new patch'verwenden, kann das Argument des ' etwas seltsam sein.

In diesem Fall ist es besser, das new_callable patchArgument von ' zu verwenden und zu beachten, dass alle zusätzlichen Argumente, patchdie nicht verwendet werden, an new_callabledie in der patchDokumentation beschriebene Funktion übergeben werden .

patch () akzeptiert beliebige Schlüsselwortargumente. Diese werden beim Bau an den Mock (oder new_callable) übergeben.

Zum Beispiel ist die dekorierte Version für Python 3.x :

@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")

Denken Sie daran, dass in diesem Fall patchdas Scheinobjekt als Argument Ihrer Testfunktion hinzugefügt wird.

Michele d'Amico
quelle
Entschuldigung, dass Sie gefragt haben, with patch("builtins.open", mock_open(read_data="data")) as mock_file:ob die Syntax in die Dekorationssyntax konvertiert werden kann. Ich habe es versucht, bin mir aber nicht sicher, worauf ich @patch("builtins.open", ...) als zweites Argument eingehen muss.
Imrek
1
@ DrunkenMaster Updateted .. danke für den Hinweis. Die Verwendung von Decorator ist in diesem Fall nicht trivial.
Michele d'Amico
Grazie! Mein Problem war etwas komplexer (ich musste das return_valuevon mock_openin ein anderes Scheinobjekt kanalisieren und das zweite Scheinobjekt behaupten return_value), aber es funktionierte durch Hinzufügen von mock_openals new_callable.
Imrek
1
@ArthurZopellaro Schauen Sie sich das sixModul an, um ein konsistentes mockModul zu erhalten. Aber ich weiß nicht, ob es auch builtinsin einem gemeinsamen Modul abgebildet ist.
Michele d'Amico
1
Wie finden Sie den richtigen Namen zum Patchen? Dh wie finden Sie das erste Argument für @patch (in diesem Fall 'builtins.open') für eine beliebige Funktion?
Zenperttu
73

Mit den neuesten Versionen von mock können Sie den wirklich nützlichen mock_open- Helfer verwenden:

mock_open (mock = Keine, read_data = Keine)

Eine Hilfsfunktion zum Erstellen eines Modells, um die Verwendung von open zu ersetzen. Es funktioniert für Open, die direkt aufgerufen oder als Kontextmanager verwendet werden.

Das Mock-Argument ist das zu konfigurierende Mock-Objekt. Wenn Keine (Standardeinstellung), wird ein MagicMock für Sie erstellt, wobei die API auf Methoden oder Attribute beschränkt ist, die in Standarddateigriffen verfügbar sind.

read_data ist eine Zeichenfolge, die die Lesemethode des Dateihandles zurückgeben soll. Dies ist standardmäßig eine leere Zeichenfolge.

>>> 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')
David
quelle
Wie überprüfen Sie, ob es mehrere .writeAnrufe gibt?
n611x007
1
@naxa Eine Möglichkeit besteht darin, jeden erwarteten Parameter an zu übergeben handle.write.assert_any_call(). Sie können auch handle.write.call_args_listjeden Anruf erhalten, wenn die Reihenfolge wichtig ist.
Rob Cutmore
m.return_value.write.assert_called_once_with('some stuff')ist besser imo. Vermeidet das Registrieren eines Anrufs.
Anonym
2
Manuelles Durchsetzen Mock.call_args_listist sicherer als das Aufrufen einer der Mock.assert_xxxMethoden. Wenn Sie eine der letzteren als Attribute von Mock falsch buchstabieren, werden sie immer stillschweigend weitergegeben.
Jonathan Hartley
12

So verwenden Sie mock_open für eine einfache Datei read()(das ursprüngliche mock_open-Snippet, das bereits auf dieser Seite angegeben ist, ist eher zum Schreiben gedacht ):

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

Beachten Sie, dass dies gemäß den Dokumenten für mock_open speziell für ist read()und daher nicht mit gängigen Mustern wie for line in fz. B. funktioniert .

Verwendet Python 2.6.6 / mock 1.0.1

jlb83
quelle
Sieht gut aus, aber ich kann es nicht mit for line in opened_file:Codetyp zum Laufen bringen. Ich habe versucht, mit iterierbarem StringIO zu experimentieren, das dies implementiert __iter__und stattdessen verwendet my_text, aber kein Glück.
Evgen
@EvgeniiPuchkaryov Dies funktioniert speziell für read()und wird in Ihrem for line in opened_fileFall nicht funktionieren . Ich habe den Beitrag bearbeitet, um zu verdeutlichen
jlb83
1
Die for line in f:Unterstützung von @EvgeniiPuchkaryov kann erreicht werden, indem stattdessen der Rückgabewert open()als StringIO-Objekt verspottet wird .
Iskar Jarak
1
Zur Verdeutlichung ist das zu testende System (SUT) in diesem Beispiel: with open("any_string") as f: print f.read()
Brad M
4

Die beste Antwort ist nützlich, aber ich habe sie etwas erweitert.

Wenn Sie den Wert Ihres Dateiobjekts (das fIn as f) basierend auf den an Folgendes übergebenen Argumenten festlegen möchten open(), haben Sie folgende Möglichkeiten:

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

Grundsätzlich open()wird ein Objekt zurückgegeben und dieses Objekt withaufgerufen __enter__().

Um richtig zu verspotten, müssen wir verspotten open(), um ein Scheinobjekt zurückzugeben. Dieses Scheinobjekt sollte dann den __enter__()Aufruf verspotten ( MagicMockwird dies für uns tun), um das Scheindaten- / Dateiobjekt zurückzugeben, das wir (daher mm.__enter__.return_value) wollen. Wenn wir dies mit 2 Mocks wie oben beschrieben tun, können wir die übergebenen Argumente erfassen open()und an unsere do_something_with_dataMethode übergeben.

Ich habe eine ganze Mock-Datei als String übergeben open()und meine do_something_with_datasah folgendermaßen aus:

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

Dadurch wird die Zeichenfolge in eine Liste umgewandelt, sodass Sie wie bei einer normalen Datei Folgendes tun können:

for line in file:
    #do action
Der Ansager
quelle
Wenn der getestete Code die Datei auf andere Weise behandelt, z. B. durch Aufrufen der Funktion "readline", können Sie jedes gewünschte Scheinobjekt in der Funktion "do_something_with_data" mit den gewünschten Attributen zurückgeben.
user3289695
Gibt es eine Möglichkeit, Berührungen zu vermeiden __enter__? Es sieht definitiv eher nach einem Hack als nach einem empfohlenen Weg aus.
Imrek
Geben Sie ist , wie Conext Manager wie open () geschrieben werden. Mocks sind oft etwas hackig, da Sie auf "private" Dinge zugreifen müssen, um sich zu verspotten, aber die Eingabe hier ist nicht fälschlicherweise hackig imo
theannouncer
3

Ich bin vielleicht etwas spät dran, aber das hat bei mir funktioniert, als ich openein anderes Modul aufgerufen habe, ohne eine neue Datei erstellen zu müssen.

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

Durch Patchen der openFunktion innerhalb des __builtin__Moduls auf my mock_open()kann ich das Schreiben in eine Datei verspotten, ohne eine zu erstellen.

Hinweis: Wenn Sie ein Modul verwenden, das Cython verwendet, oder Ihr Programm in irgendeiner Weise von Cython abhängt, müssen Sie das Cython- __builtin__Modul importieren , indem Sie es import __builtin__oben in Ihre Datei aufnehmen. Sie werden nicht in der Lage sein, das Universelle zu verspotten, __builtin__wenn Sie Cython verwenden.

Leo C Han
quelle
Eine Variation dieses Ansatzes funktionierte für mich, da sich der Großteil des getesteten Codes in anderen Modulen befand, wie hier gezeigt. Ich musste sicherstellen, dass import __builtin__mein Testmodul erweitert wurde. Dieser Artikel half zu klären, warum diese Technik so gut funktioniert wie sie: ichimonji10.name/blog/6
killthrush
0

So patchen Sie die integrierte open () - Funktion mit unittest:

Dies funktionierte für einen Patch zum Lesen einer JSON-Konfiguration.

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

Das verspottete Objekt ist das von der Funktion open () zurückgegebene io.TextIOWrapper-Objekt

@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):
pabloberm
quelle
0

Wenn Sie keine weitere Datei benötigen, können Sie die Testmethode dekorieren:

@patch('builtins.open', mock_open(read_data="data"))
def test_testme():
    result = testeme()
    assert result == "data"
Ferdinando de Melo
quelle