Come faccio a deridere un open usato in un'istruzione with (usando il framework Mock in Python)?


188

Come testare il codice seguente con i mock (usando i mock, il decoratore di patch e le sentinelle fornite dal framework Mock di Michael Foord ):

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

@Daryl Spitzer: potresti lasciar perdere la meta-domanda ("Conosco la risposta ...") È confusa.
S.Lott

In passato, quando l'ho lasciato fuori, la gente si è lamentata del fatto che sto rispondendo alla mia domanda. Proverò a spostarlo sulla mia risposta.
Daryl Spitzer,

1
@Daryl: Il modo migliore per evitare lamentele riguardo alla risposta alla propria domanda, che di solito deriva dalle preoccupazioni del "puttanaggio del karma", è quello di contrassegnare la domanda e / o la risposta come "wiki della comunità".
John Millikin,

3
Se rispondere alla tua domanda è considerato Karma Whoring, penso che le FAQ dovrebbero essere chiarite su questo punto.
EBGreen

Risposte:


132

Il modo per farlo è cambiato in mock 0.7.0 che finalmente supporta il deridere i metodi del protocollo python (metodi magici), in particolare usando MagicMock:

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

Un esempio di derisione aperto come gestore di contesto (dalla pagina degli esempi nella documentazione di simulazione):

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

Wow! Questo sembra molto più semplice dell'esempio del gestore del contesto attualmente disponibile su voidspace.org.uk/python/mock/magicmock.html che imposta esplicitamente __enter__e __exit__beffa anche gli oggetti: quest'ultimo approccio è obsoleto o è ancora utile?
Brandon Rodi,

6
L '"ultimo approccio" sta mostrando come farlo senza usare un MagicMock (cioè è solo un esempio di come Mock supporta i metodi magici). Se usi un MagicMock (come sopra), entra e esci sono preconfigurate per te.
fuzzyman,

5
potresti indicare il tuo post sul blog in cui spieghi più in dettaglio perché / come funziona
Rodrigue,

9
In Python 3, 'file' non è definito (usato nelle specifiche MagicMock), quindi sto usando io.IOBase.
Jonathan Hartley,

1
Nota: in Python3 l'integrato fileè sparito!
exhuma,

239

mock_openfa parte del mockframework ed è molto semplice da usare. patchusato come contesto restituisce l'oggetto usato per sostituire quello patchato: puoi usarlo per semplificare il tuo test.

Python 3.x

Usa builtinsinvece di __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

mocknon fa parte di unitteste dovresti patch__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")

Custodia per decoratore

Se patchusassi come decoratore usando mock_open()il risultato come new patchargomento, può essere un po 'strano.

In questo caso è meglio usare l' new_callable patchargomento di e ricordare che tutti gli argomenti extra che patchnon usano saranno passati per new_callablefunzionare come descritto nella patchdocumentazione .

patch () accetta argomenti di parole chiave arbitrari. Questi saranno passati al Mock (o new_callable) durante la costruzione.

Ad esempio la versione decorata per 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")

Ricorda che in questo caso patchaggiungerai l'oggetto finto come argomento della tua funzione di test.


Ci scusiamo per averlo chiesto, può with patch("builtins.open", mock_open(read_data="data")) as mock_file:essere convertito in sintassi del decoratore? Ci ho provato, ma non sono sicuro di cosa devo passare @patch("builtins.open", ...) come secondo argomento.
imrek,

1
@DrunkenMaster Aggiornato .. grazie per averlo sottolineato. L'uso di decoratore non è banale in questo caso.
Michele d'Amico,

Grazie! Il mio problema era un po 'più complesso (dovevo incanalare il return_valueof mock_openin un altro oggetto finto e affermare il secondo mock return_value), ma ha funzionato aggiungendo mock_openas new_callable.
imrek,

1
@ArthurZopellaro dai un'occhiata al sixmodulo per avere un mockmodulo coerente . Ma non so se mappare anche builtinsin un modulo comune.
Michele d'Amico,

1
Come si trova il nome corretto da patchare? Vale a dire come trovare il primo argomento su @patch ('builtins.open' in questo caso) per una funzione arbitraria?
zenperttu,

73

Con le ultime versioni di mock, puoi usare l'utilissimo mock_open helper:

mock_open (mock = None, read_data = None)

Una funzione di aiuto per creare un finto per sostituire l'uso di open. Funziona per open chiamato direttamente o utilizzato come gestore di contesto.

L'argomento finto è l'oggetto finto da configurare. Se Nessuno (impostazione predefinita), verrà creato per te un MagicMock, con l'API limitata ai metodi o agli attributi disponibili sugli handle di file standard.

read_data è una stringa per la restituzione del metodo read dell'handle del file. Questa è una stringa vuota per impostazione predefinita.

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

come controllate se ci sono più .writechiamate?
n611x007,

1
@naxa Un modo è passare ogni parametro previsto a handle.write.assert_any_call(). Puoi anche usare handle.write.call_args_listper ricevere ogni chiamata se l'ordine è importante.
Rob Cutmore,

m.return_value.write.assert_called_once_with('some stuff')è meglio imo. Evita di registrare una chiamata.
Anonimo

2
Affermare manualmente Mock.call_args_listè più sicuro che chiamare uno dei Mock.assert_xxxmetodi. Se si scrive erroneamente uno di questi ultimi, essendo attributi di Mock, passeranno sempre silenziosamente.
Jonathan Hartley,

12

Per utilizzare mock_open per un file semplice read()(lo snippet mock_open originale già indicato in questa pagina è più orientato alla scrittura):

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

Nota come da documenti per mock_open, questo è specifico per read(), quindi non funzionerà con schemi comuni come for line in f, ad esempio.

Utilizza python 2.6.6 / mock 1.0.1


Sembra buono, ma non riesco a farlo funzionare con il for line in opened_file:tipo di codice. Ho provato a sperimentare con StringIO iterabile che implementa __iter__e usando quello invece di my_text, ma senza fortuna.
Evgen,

@EvgeniiPuchkaryov Funziona in modo specifico, read()quindi non funzionerà nel tuo for line in opened_filecaso; Ho modificato il post per chiarire
jlb83,

1
Il for line in f:supporto di @EvgeniiPuchkaryov può essere ottenuto deridendo invece il valore restituito open()come oggetto StringIO .
Iskar Jarak,

1
Per chiarire, il sistema sotto test (SUT) in questo esempio è: with open("any_string") as f: print f.read()
Brad M

4

La risposta migliore è utile ma l'ho ampliata un po '.

Se si desidera impostare il valore dell'oggetto file ( fin as f) in base agli argomenti passati a open()questo è un modo per farlo:

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

Fondamentalmente, open()restituirà un oggetto e withchiamerà __enter__()quell'oggetto.

Per deridere correttamente, dobbiamo deridere open()per restituire un oggetto finto. Quell'oggetto finto dovrebbe quindi deridere la __enter__()chiamata su di esso ( MagicMocklo farà per noi) per restituire l'oggetto dati / file fittizio che vogliamo (quindi mm.__enter__.return_value). In questo modo con 2 beffe il modo sopra ci permette di catturare gli argomenti passati open()e passarli al nostro do_something_with_datametodo.

Ho passato un intero file simulato come una stringa open()e il mio do_something_with_datasembrava così:

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

Questo trasforma la stringa in un elenco in modo da poter eseguire le seguenti operazioni come con un normale file:

for line in file:
    #do action

Se il codice in fase di test gestisce il file in modo diverso, ad esempio chiamando la sua funzione "readline", è possibile restituire qualsiasi oggetto fittizio desiderato nella funzione "do_something_with_data" con gli attributi desiderati.
user3289695,

C'è un modo per evitare di toccare __enter__? Sembra decisamente più un trucco che un modo consigliato.
imrek,

enter è come vengono scritti i gestori di conext come open (). Mocks sarà spesso un po 'hacky in che è necessario l'accesso roba "privato" per finta, ma il inserisci qui isnt ingerintly hacky imo
theannouncer

3

Potrei essere un po 'in ritardo al gioco, ma questo ha funzionato per me quando ho chiamato openun altro modulo senza dover creare un nuovo file.

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

Patchando la openfunzione all'interno del __builtin__modulo sul mio mock_open(), posso deridere la scrittura su un file senza crearne uno.

Nota: se si utilizza un modulo che utilizza cython o il programma dipende in qualche modo da cython, sarà necessario importare il __builtin__modulo cython includendolo import __builtin__nella parte superiore del file. Non sarai in grado di deridere l'universale __builtin__se stai usando cython.


Una variante di questo approccio ha funzionato per me, poiché la maggior parte del codice in esame era in altri moduli, come mostrato qui. Ho dovuto assicurarmi di aggiungere import __builtin__al mio modulo di test. Questo articolo ha aiutato a chiarire perché questa tecnica funziona così come funziona: ichimonji10.name/blog/6
killthrush

0

Per patchare la funzione open () integrata con unittest:

Questo ha funzionato per una patch per leggere una configurazione JSON.

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

L'oggetto deriso è l'oggetto io.TextIOWrapper restituito dalla funzione 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

Se non hai bisogno di altri file, puoi decorare il metodo di test:

@patch('builtins.open', mock_open(read_data="data"))
def test_testme():
    result = testeme()
    assert result == "data"
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.