¿Cómo me burlo de un abierto usado en una declaración with (usando el marco Mock en Python)?

188

¿Cómo pruebo el siguiente código con simulacros (utilizando simulacros, el decorador de parches y los centinelas proporcionados por el marco simulado de Michael Foord ):

def testme(filepath):
    with open(filepath, 'r') as f:
        return f.read()
Daryl Spitzer
fuente
@Daryl Spitzer: ¿podrías dejar la meta-pregunta ("Sé la respuesta ...") Es confuso.
S.Lott
En el pasado, cuando lo dejé, la gente se quejaba de que estaba respondiendo mi propia pregunta. Intentaré mover eso a mi respuesta.
Daryl Spitzer
1
@Daryl: La mejor manera de evitar quejas sobre responder a la propia pregunta, que generalmente se derivan de las preocupaciones de "prostitución de karma", es marcar la pregunta y / o respuesta como un "wiki de la comunidad".
John Millikin
3
Si responder a su propia pregunta se considera Karma Whoring, creo que las preguntas frecuentes deben aclararse sobre ese punto.
EBGreen

Respuestas:

132

La forma de hacer esto ha cambiado en simulacro 0.7.0 que finalmente admite burlarse de los métodos de protocolo de Python (métodos mágicos), particularmente usando MagicMock:

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

Un ejemplo de simulacro abierto como administrador de contexto (de la página de ejemplos en la documentación simulada):

>>> 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
fuente
¡Guauu! Esto parece mucho más simple que el ejemplo de administrador de contexto actualmente en voidspace.org.uk/python/mock/magicmock.html que establece explícitamente __enter__y también se __exit__burla de los objetos: ¿este último enfoque está desactualizado o sigue siendo útil?
Brandon Rhodes
66
El "último enfoque" muestra cómo hacerlo sin usar un MagicMock (es decir, es solo un ejemplo de cómo Mock admite métodos mágicos). Si usa un MagicMock (como se indicó anteriormente), entre y salga previamente configurado.
fuzzyman
55
podría señalar su publicación de blog donde explica con más detalles por qué / cómo funciona
Rodrigue
9
En Python 3, 'archivo' no está definido (usado en la especificación MagicMock), así que estoy usando io.IOBase en su lugar.
Jonathan Hartley
1
Nota: ¡en Python3 la construcción filese ha ido!
exhuma
239

mock_openes parte del mockframework y es muy simple de usar. patchusado como contexto devuelve el objeto usado para reemplazar el parcheado: puede usarlo para simplificar su prueba.

Python 3.x

Usar en builtinslugar 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

mockno es parte unittesty debes parchear__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")

Estuche decorador

Si patchusarías como decorador usando mock_open()el resultado como new patchargumento, puede ser un poco extraño.

En este caso, es mejor usar el new_callable patchargumento de 's y recordar que cada argumento adicional que patchno use se pasará a new_callablefuncionar como se describe en la patchdocumentación .

patch () toma argumentos de palabras clave arbitrarios. Estos se pasarán al simulacro (o new_callable) en la construcción.

Por ejemplo, la versión decorada para Python 3.x es:

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

Recuerde que en este caso patchagregará el objeto simulado como argumento de su función de prueba.

Michele d'Amico
fuente
Perdón por preguntar, ¿se with patch("builtins.open", mock_open(read_data="data")) as mock_file:puede convertir en sintaxis decoradora? Lo he intentado, pero no estoy seguro de lo que necesito pasar @patch("builtins.open", ...) como segundo argumento.
Imrek
1
@DrunkenMaster actualizado ... gracias por apuntarlo. Usar decorador no es trivial en este caso.
Michele d'Amico
Grazie! Mi problema era un poco más complejo (tuve que canalizar el return_valuede mock_openen otro objeto simulado y afirmar el segundo simulacro return_value), pero funcionó agregando mock_opencomo new_callable.
Imrek
1
@ArthurZopellaro eche un vistazo al sixmódulo para tener un mockmódulo consistente . Pero no sé si se asigna también builtinsen un módulo común.
Michele d'Amico
1
¿Cómo encuentras el nombre correcto para parchear? Es decir, ¿cómo encontrar el primer argumento para @patch ('builtins.open' en este caso) para una función arbitraria?
zenperttu el
73

Con las últimas versiones de simulacro, puede usar el ayudante realmente útil mock_open :

mock_open (simulacro = Ninguno, read_data = Ninguno)

Una función auxiliar para crear un simulacro para reemplazar el uso de open. Funciona para llamadas abiertas directamente o se usa como administrador de contexto.

El argumento simulado es el objeto simulado a configurar. Si ninguno (el valor predeterminado), se creará un MagicMock para usted, con la API limitada a los métodos o atributos disponibles en los identificadores de archivos estándar.

read_data es una cadena para que el método de lectura del identificador de archivo regrese. Esta es una cadena vacía por defecto.

>>> 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
fuente
¿Cómo verifica si hay varias .writellamadas?
n611x007
1
@naxa Una forma es pasar cada parámetro esperado a handle.write.assert_any_call(). También puede usar handle.write.call_args_listpara recibir cada llamada si el pedido es importante.
Rob Cutmore
m.return_value.write.assert_called_once_with('some stuff')es mejor imo. Evita registrar una llamada.
Anónimo
2
Afirmar manualmente Mock.call_args_listes más seguro que llamar a cualquiera de los Mock.assert_xxxmétodos. Si deletrea mal alguno de estos últimos, siendo atributos de Mock, siempre pasarán en silencio.
Jonathan Hartley
12

Para usar mock_open para un archivo simple read()(el fragmento original de mock_open ya proporcionado en esta página está más orientado a la escritura):

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

Tenga en cuenta según los documentos para mock_open, esto es específicamente para read(), por lo que no funcionará con patrones comunes como for line in f, por ejemplo.

Utiliza python 2.6.6 / simulacro 1.0.1

jlb83
fuente
Se ve bien, pero no puedo hacer que funcione con el for line in opened_file:tipo de código. Intenté experimentar con StringIO iterable que implementa __iter__y usar eso en lugar de my_text, pero no tuve suerte.
Evgen
@EvgeniiPuchkaryov Esto funciona específicamente para read()que no funcione en su for line in opened_filecaso; He editado la publicación para aclarar
jlb83
1
El for line in f:soporte de @EvgeniiPuchkaryov se puede lograr burlándose del valor de retorno open()como un objeto StringIO .
Iskar Jarak
1
Para aclarar, el sistema bajo prueba (SUT) en este ejemplo es: with open("any_string") as f: print f.read()
Brad M
4

La respuesta principal es útil, pero la amplié un poco.

Si desea establecer el valor de su objeto de archivo (la entrada ) fen as ffunción de los argumentos pasados, open()aquí hay una forma de hacerlo:

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

Básicamente, open()devolverá un objeto y withllamará __enter__()a ese objeto.

Para burlarse correctamente, debemos burlarnos open()para devolver un objeto simulado. Ese objeto simulado debe simular la __enter__()llamada ( MagicMockhará esto por nosotros) para devolver el objeto simulado de datos / archivo que queremos (por lo tanto mm.__enter__.return_value). Hacer esto con 2 simulacros de la manera anterior nos permite capturar los argumentos pasados open()y pasarlos a nuestro do_something_with_datamétodo.

Pasé un archivo simulado completo como una cadena open()y mi do_something_with_dataaspecto era el siguiente:

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

Esto transforma la cadena en una lista para que pueda hacer lo siguiente como lo haría con un archivo normal:

for line in file:
    #do action
el anunciador
fuente
Si el código que se prueba maneja el archivo de una manera diferente, por ejemplo, llamando a su función "readline", puede devolver cualquier objeto simulado que desee en la función "do_something_with_data" con los atributos deseados.
user3289695
¿Hay alguna manera de evitar tocar __enter__? Definitivamente parece más un truco que una forma recomendada.
imrek
enter es cómo se escriben los administradores de conext como open (). Los simulacros a menudo serán un poco extravagantes en el sentido de que debes acceder a cosas "privadas" para burlarte, pero el ingreso aquí no es ingerintivamente imo
anunciador el
3

Puede que llegue un poco tarde al juego, pero esto funcionó cuando llamé opena otro módulo sin tener que crear un nuevo archivo.

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

Al parchear la openfunción dentro del __builtin__módulo a mi mock_open(), puedo simular escribir en un archivo sin crear uno.

Nota: Si está utilizando un módulo que usa cython, o si su programa depende de cython de alguna manera, deberá importar el __builtin__módulo de cython al incluirlo import __builtin__en la parte superior de su archivo. No podrá burlarse de lo universal __builtin__si está utilizando cython.

Leo C Han
fuente
Una variación de este enfoque funcionó para mí, ya que la mayoría del código bajo prueba estaba en otros módulos como se muestra aquí. Necesitaba asegurarme de agregar import __builtin__a mi módulo de prueba. Este artículo ayudó a aclarar por qué esta técnica funciona tan bien como lo hace: ichimonji10.name/blog/6
killthrush
0

Para parchear la función incorporada open () con unittest:

Esto funcionó para un parche para leer una configuración json.

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

El objeto burlado es el objeto io.TextIOWrapper devuelto por la función 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
fuente
0

Si no necesita más archivos, puede decorar el método de prueba:

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