Comment simuler un open utilisé dans une instruction with (en utilisant le framework Mock en Python)?

188

Comment tester le code suivant avec des simulacres (en utilisant des simulations, le décorateur de patchs et les sentinelles fournis par le framework Mock de Michael Foord ):

def testme(filepath):
    with open(filepath, 'r') as f:
        return f.read()
Daryl Spitzer
la source
@Daryl Spitzer: pourriez-vous laisser de côté la méta-question ("Je connais la réponse ...") C'est déroutant.
S.Lott
Dans le passé, quand je l'ai laissé de côté, les gens se sont plaints que je répondais à ma propre question. Je vais essayer de déplacer cela vers ma réponse.
Daryl Spitzer
1
@Daryl: La meilleure façon d'éviter les plaintes concernant la réponse à sa propre question, qui découlent généralement des soucis de "karma putain", est de marquer la question et / ou la réponse comme un "wiki communautaire".
John Millikin
3
Si la réponse à votre propre question est considérée comme Karma Whoring, la FAQ devrait être clarifiée sur ce point, je pense.
EBGreen

Réponses:

132

La façon de faire a changé dans mock 0.7.0 qui prend enfin en charge la moquerie des méthodes de protocole python (méthodes magiques), en particulier en utilisant MagicMock:

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

Un exemple de mocking ouvert en tant que gestionnaire de contexte (à partir de la page d'exemples dans la documentation fictive):

>>> 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
la source
Hou la la! Cela semble beaucoup plus simple que l'exemple de gestionnaire de contexte actuellement à voidspace.org.uk/python/mock/magicmock.html qui définit explicitement __enter__et simule également__exit__ des objets - cette dernière approche est-elle obsolète ou toujours utile?
Brandon Rhodes
6
La «dernière approche» montre comment le faire sans utiliser MagicMock (c'est-à-dire qu'il s'agit simplement d'un exemple de la façon dont Mock supporte les méthodes magiques). Si vous utilisez un MagicMock (comme ci-dessus), l' entrée et la sortie sont préconfigurées pour vous.
fuzzyman
5
vous pouvez pointer vers votre article de blog où vous expliquez plus en détail pourquoi / comment cela fonctionne
Rodrigue
9
Dans Python 3, 'fichier' n'est pas défini (utilisé dans la spécification MagicMock), j'utilise donc io.IOBase à la place.
Jonathan Hartley
1
Remarque: dans Python3, le builtin fileest parti!
exhuma le
239

mock_openfait partie du mockframework et est très simple à utiliser. patchutilisé comme contexte renvoie l'objet utilisé pour remplacer celui qui a été corrigé: vous pouvez l'utiliser pour simplifier votre test.

Python 3.x

Utilisez à la builtinsplace de __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

mockne fait pas partie de unittestet vous devriez patcher__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")

Coffret décorateur

Si vous patchutilisiez comme décorateur en utilisant mock_open()le résultat de s comme new patchargument de s, cela peut être un peu bizarre.

Dans ce cas, il est préférable d 'utiliser l new_callable patch' argument s et de se rappeler que tous les arguments supplémentaires qui patchne sont pas utilisés seront passés à la new_callablefonction comme décrit dans la patchdocumentation .

patch () prend des arguments de mot-clé arbitraires. Ceux-ci seront transmis au Mock (ou new_callable) lors de la construction.

Par exemple, la version décorée pour Python 3.x est:

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

N'oubliez pas que dans ce cas patch, vous ajouterez l'objet fictif comme argument de votre fonction de test.

Michèle d'Amico
la source
Pardon de vous avoir demandé, peut-il with patch("builtins.open", mock_open(read_data="data")) as mock_file:être converti en syntaxe de décorateur? J'ai essayé, mais je ne sais pas dans quoi je dois passer @patch("builtins.open", ...) comme deuxième argument.
imrek
1
@DrunkenMaster mis à jour .. merci de l'avoir signalé. L'utilisation de decorator n'est pas anodine dans ce cas.
Michele d'Amico
Grazie! Mon problème était un peu plus complexe (je devais canaliser le return_valueof mock_opendans un autre objet fictif et affirmer le second mock's return_value), mais cela fonctionnait en ajoutant mock_openas new_callable.
imrek
1
@ArthurZopellaro jetez un œil au sixmodule pour avoir un mockmodule cohérent . Mais je ne sais pas si cela correspond également builtinsà un module commun.
Michele d'Amico
1
Comment trouver le nom correct à corriger? C'est-à-dire comment trouver le premier argument de @patch ('builtins.open' dans ce cas) pour une fonction arbitraire?
zenperttu
73

Avec les dernières versions de mock, vous pouvez utiliser l' assistant mock_open vraiment utile :

mock_open (mock = None, read_data = None)

Une fonction d'assistance pour créer une maquette pour remplacer l'utilisation de open. Il fonctionne pour ouvrir appelé directement ou utilisé comme gestionnaire de contexte.

L'argument fictif est l'objet fictif à configurer. Si Aucun (valeur par défaut), un MagicMock sera créé pour vous, avec l'API limitée aux méthodes ou attributs disponibles sur les descripteurs de fichiers standard.

read_data est une chaîne que la méthode de lecture du descripteur de fichier doit renvoyer. Il s'agit d'une chaîne vide par défaut.

>>> 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
la source
comment vérifier s'il y a plusieurs .writeappels?
n611x007
1
@naxa Une façon est de passer chaque paramètre attendu à handle.write.assert_any_call(). Vous pouvez également utiliser handle.write.call_args_listpour recevoir chaque appel si la commande est importante.
Rob Cutmore
m.return_value.write.assert_called_once_with('some stuff')est mieux imo. Évite d'enregistrer un appel.
Anonyme le
2
Il Mock.call_args_listest plus sûr de déclarer manuellement about que d'appeler l'une des Mock.assert_xxxméthodes,. Si vous mal orthographiez l'un de ces derniers, étant des attributs de Mock, ils passeront toujours silencieusement.
Jonathan Hartley
12

Pour utiliser mock_open pour un fichier simple read()(l'extrait de code original mock_open déjà donné sur cette page est davantage destiné à l'écriture):

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

Notez que selon la documentation pour mock_open, ceci est spécifiquement pour read(), donc ne fonctionnera pas avec des modèles courants comme for line in f, par exemple.

Utilise python 2.6.6 / mock 1.0.1

jlb83
la source
Ça a l'air bien, mais je ne peux pas le faire fonctionner avec un for line in opened_file:type de code. J'ai essayé d'expérimenter avec StringIO itérable qui implémente __iter__et de l'utiliser à la place de my_text, mais pas de chance.
Evgen
@EvgeniiPuchkaryov Cela fonctionne spécifiquement pour read()donc ne fonctionnera pas dans votre for line in opened_filecas; J'ai édité le message pour clarifier
jlb83
1
La prise en for line in f:charge de @EvgeniiPuchkaryov peut être obtenue en se moquant de la valeur de retour de en open()tant qu'objet StringIO .
Iskar Jarak
1
Pour clarifier, le système sous test (SUT) dans cet exemple est: with open("any_string") as f: print f.read()
Brad M
4

La première réponse est utile, mais je l'ai un peu développée.

Si vous souhaitez définir la valeur de votre objet fichier (le fin as f) en fonction des arguments passés, open()voici une façon de le faire:

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

Fondamentalement, open()retournera un objet et withappellera __enter__()cet objet.

Pour se moquer correctement, il faut se moquer open()pour renvoyer un objet factice. Cet objet factice devrait alors se moquer de l' __enter__()appel (le MagicMockfera pour nous) pour renvoyer l'objet de données / fichier factice que nous voulons (par conséquent mm.__enter__.return_value). Faire cela avec 2 mocks comme ci-dessus nous permet de capturer les arguments passés à open()et de les transmettre à notre do_something_with_dataméthode.

J'ai passé un fichier simulé entier sous forme de chaîne à open()et mon do_something_with_dataressemblait à ceci:

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

Cela transforme la chaîne en une liste afin que vous puissiez faire ce qui suit comme vous le feriez avec un fichier normal:

for line in file:
    #do action
l'annonceur
la source
Si le code en cours de test traite le fichier d'une manière différente, par exemple en appelant sa fonction "readline", vous pouvez renvoyer n'importe quel objet factice que vous voulez dans la fonction "do_something_with_data" avec les attributs souhaités.
user3289695
Existe-t-il un moyen d'éviter de toucher __enter__? Cela ressemble définitivement plus à un hack qu'à une méthode recommandée.
imrek le
enter est la façon dont les gestionnaires de conext comme open () sont écrits. Les mocks seront souvent un peu hacky dans la mesure où vous devez accéder à des trucs "privés" pour se moquer, mais l'entrée ici n'est pas complètement hacky imo
theannouncer
3

Je suis peut-être un peu en retard dans le jeu, mais cela a fonctionné pour moi lors de l'appel opend'un autre module sans avoir à créer un nouveau fichier.

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

En patchant la openfonction à l'intérieur du __builtin__module sur my mock_open(), je peux me moquer de l'écriture dans un fichier sans en créer un.

Remarque: Si vous utilisez un module qui utilise cython, ou que votre programme dépend de cython de quelque manière que ce soit, vous devrez importer le __builtin__module de cython en l'incluant import __builtin__en haut de votre fichier. Vous ne pourrez pas vous moquer de l'universel __builtin__si vous utilisez cython.

Leo C Han
la source
Une variante de cette approche a fonctionné pour moi, car la majorité du code testé était dans d'autres modules, comme indiqué ici. J'ai eu besoin de m'assurer d'ajouter import __builtin__à mon module de test. Cet article a aidé à clarifier pourquoi cette technique fonctionne aussi bien: ichimonji10.name/blog/6
killthrush
0

Pour patcher la fonction open () intégrée avec unittest:

Cela a fonctionné pour un correctif pour lire une configuration json.

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

L'objet simulé est l'objet io.TextIOWrapper renvoyé par la fonction 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):
pabloberm
la source
0

Si vous n'avez plus besoin de fichier, vous pouvez décorer la méthode de test:

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