¿Cómo afirmar la salida con nosetest / unittest en Python?

114

Estoy escribiendo pruebas para una función como la siguiente:

def foo():
    print 'hello world!'

Entonces, cuando quiera probar esta función, el código será así:

import sys
from foomodule import foo
def test_foo():
    foo()
    output = sys.stdout.getline().strip() # because stdout is an StringIO instance
    assert output == 'hello world!'

Pero si ejecuto nosetests con el parámetro -s, la prueba falla. ¿Cómo puedo captar la salida con unittest o módulo de nariz?

Pedro Valencia
fuente

Respuestas:

125

Utilizo este administrador de contexto para capturar la salida. En última instancia, utiliza la misma técnica que algunas de las otras respuestas reemplazando temporalmente sys.stdout. Prefiero el administrador de contexto porque envuelve toda la contabilidad en una sola función, por lo que no tengo que volver a escribir ningún código de prueba final, y no tengo que escribir funciones de configuración y desmontaje solo para esto.

import sys
from contextlib import contextmanager
from StringIO import StringIO

@contextmanager
def captured_output():
    new_out, new_err = StringIO(), StringIO()
    old_out, old_err = sys.stdout, sys.stderr
    try:
        sys.stdout, sys.stderr = new_out, new_err
        yield sys.stdout, sys.stderr
    finally:
        sys.stdout, sys.stderr = old_out, old_err

Úselo así:

with captured_output() as (out, err):
    foo()
# This can go inside or outside the `with` block
output = out.getvalue().strip()
self.assertEqual(output, 'hello world!')

Además, dado que el estado de salida original se restaura al salir del withbloque, podemos configurar un segundo bloque de captura en la misma función que el primero, lo cual no es posible usando las funciones de configuración y desmontaje, y se vuelve prolijo al escribir try-finalmente bloques manualmente. Esa capacidad resultó útil cuando el objetivo de una prueba era comparar los resultados de dos funciones entre sí en lugar de con algún valor precalculado.

Rob Kennedy
fuente
Esto me ha funcionado muy bien en pep8radius . Sin embargo, recientemente había usado esto nuevamente y obtenía el siguiente error al imprimir TypeError: unicode argument expected, got 'str'(el tipo pasado para imprimir (str / unicode) es irrelevante).
Andy Hayden
9
Hmmm puede ser que en Python 2 queramos from io import BytesIO as StringIOy en Python 3 solo from io import StringIO. Creo que pareció solucionar el problema en mis pruebas.
Andy Hayden
4
Ooop, solo para terminar, disculpas por tantos mensajes. Solo para aclarar a las personas que encuentran esto: python3 usa io.StringIO, python 2 usa StringIO.StringIO! ¡Gracias de nuevo!
Andy Hayden
¿Por qué todos los ejemplos aquí invocan strip()al unicodedevuelto StringIO.getvalue()?
Palimondo
1
No, @Vedran. Esto se basa en volver a vincular el nombre al que pertenece sys. Con su declaración de importación, está creando una variable local llamada stderrque recibió una copia del valor en formato sys.stderr. Los cambios en uno no se reflejan en el otro.
Rob Kennedy
60

Si realmente desea hacer esto, puede reasignar sys.stdout durante la prueba.

def test_foo():
    import sys
    from foomodule import foo
    from StringIO import StringIO

    saved_stdout = sys.stdout
    try:
        out = StringIO()
        sys.stdout = out
        foo()
        output = out.getvalue().strip()
        assert output == 'hello world!'
    finally:
        sys.stdout = saved_stdout

Sin embargo, si estuviera escribiendo este código, preferiría pasar un outparámetro opcional a la foofunción.

def foo(out=sys.stdout):
    out.write("hello, world!")

Entonces la prueba es mucho más simple:

def test_foo():
    from foomodule import foo
    from StringIO import StringIO

    out = StringIO()
    foo(out=out)
    output = out.getvalue().strip()
    assert output == 'hello world!'
Shane Hathaway
fuente
11
Nota: En python 3.x, la StringIOclase ahora debe importarse desde el iomódulo. from io import StringIOfunciona en python 2.6+.
Bryan P
2
Si usa from io import StringIOen python 2, obtiene un TypeError: unicode argument expected, got 'str'al imprimir.
matiasg
9
Nota rápida: en Python 3.4, puede usar el administrador de contexto contextlib.redirect_stdout para hacer esto de una manera que sea segura para excepciones:with redirect_stdout(out):
Lucretiel
2
No es necesario que lo haga saved_stdout = sys.stdout, siempre tiene una referencia mágica para esto en sys.__stdout__, por ejemplo, solo lo necesita sys.stdout = sys.__stdout__en su limpieza.
ThorSummoner
@ThorSummoner Gracias, esto acaba de simplificar algunas de mis pruebas ... de buceo que veo que has protagonizado ... ¡mundo pequeño!
Jonathon Reinhart
48

Desde la versión 2.7, ya no necesita reasignar sys.stdout, esto se proporciona a través de bufferflag . Además, es el comportamiento predeterminado de nosetest.

Aquí hay un ejemplo de falla en un contexto sin búfer:

import sys
import unittest

def foo():
    print 'hello world!'

class Case(unittest.TestCase):
    def test_foo(self):
        foo()
        if not hasattr(sys.stdout, "getvalue"):
            self.fail("need to run in buffered mode")
        output = sys.stdout.getvalue().strip() # because stdout is an StringIO instance
        self.assertEquals(output,'hello world!')

Puede configurar búfer a través de unit2la línea de órdenes -b, --buffero en unittest.mainopciones. Lo contrario se consigue mediante nosetestbandera --nocapture.

if __name__=="__main__":   
    assert not hasattr(sys.stdout, "getvalue")
    unittest.main(module=__name__, buffer=True, exit=False)
    #.
    #----------------------------------------------------------------------
    #Ran 1 test in 0.000s
    #
    #OK
    assert not hasattr(sys.stdout, "getvalue")

    unittest.main(module=__name__, buffer=False)
    #hello world!
    #F
    #======================================================================
    #FAIL: test_foo (__main__.Case)
    #----------------------------------------------------------------------
    #Traceback (most recent call last):
    #  File "test_stdout.py", line 15, in test_foo
    #    self.fail("need to run in buffered mode")
    #AssertionError: need to run in buffered mode
    #
    #----------------------------------------------------------------------
    #Ran 1 test in 0.002s
    #
    #FAILED (failures=1)
FabienAndre
fuente
Tenga en cuenta que esto interactúa con --nocapture; en particular, si se establece esta bandera, el modo en búfer se desactivará. Por lo tanto, tiene la opción de poder ver la salida en el terminal o de poder probar que la salida es la esperada.
ijoseph
1
¿Es posible activar y desactivar esto para cada prueba, porque esto hace que la depuración sea muy difícil cuando se usa algo como ipdb.set_trace ()?
Lqueryvg
33

Muchas de estas respuestas me fallaron porque no puedes from StringIO import StringIOen Python 3. Aquí hay un fragmento de trabajo mínimo basado en el comentario de @ naxa y el Libro de recetas de Python.

from io import StringIO
from unittest.mock import patch

with patch('sys.stdout', new=StringIO()) as fakeOutput:
    print('hello world')
    self.assertEqual(fakeOutput.getvalue().strip(), 'hello world')
Noumenon
fuente
3
Me encanta este para Python 3, ¡está limpio!
Sylhare
1
¡Esta fue la única solución en esta página que funcionó para mí! Gracias.
Justin Eyster
24

En python 3.5 puede usar contextlib.redirect_stdout()y StringIO(). Aquí está la modificación de su código

import contextlib
from io import StringIO
from foomodule import foo

def test_foo():
    temp_stdout = StringIO()
    with contextlib.redirect_stdout(temp_stdout):
        foo()
    output = temp_stdout.getvalue().strip()
    assert output == 'hello world!'
Mudit Jain
fuente
¡Gran respuesta! Según la documentación, esto se agregó en Python 3.4.
Hypercube
Es 3.4 para redirect_stdout y 3.5 para redirect_stderr. ¡quizás ahí es donde surgió la confusión!
rbennell
redirect_stdout()y redirect_stderr()devuelve su argumento de entrada. Entonces, with contextlib.redirect_stdout(StringIO()) as temp_stdout:te da todo en una línea. Probado con 3.7.1.
Adrian W
17

Recién estoy aprendiendo Python y me encontré luchando con un problema similar al anterior con pruebas unitarias para métodos con salida. Mi prueba unitaria aprobada para el módulo foo anterior terminó luciendo así:

import sys
import unittest
from foo import foo
from StringIO import StringIO

class FooTest (unittest.TestCase):
    def setUp(self):
        self.held, sys.stdout = sys.stdout, StringIO()

    def test_foo(self):
        foo()
        self.assertEqual(sys.stdout.getvalue(),'hello world!\n')
sean_robbins
fuente
5
Es posible que desee hacer una sys.stdout.getvalue().strip()y no hacer trampa en comparación con \n:)
Silviu
El módulo StringIO está en desuso. En cambiofrom io import StringIO
Edwarric
10

Escribir pruebas a menudo nos muestra una mejor manera de escribir nuestro código. Similar a la respuesta de Shane, me gustaría sugerir otra forma de ver esto. ¿Realmente quiere afirmar que su programa generó una determinada cadena, o simplemente que construyó una determinada cadena para la salida? Esto se vuelve más fácil de probar, ya que probablemente podamos asumir que la printdeclaración de Python hace su trabajo correctamente.

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

Entonces tu prueba es muy simple:

def test_foo_msg():
    assert 'hello world' == foo_msg()

Por supuesto, si realmente necesita probar la salida real de su programa, no dude en ignorarlo. :)

Alison R.
fuente
1
pero en este caso no se probará foo ... quizás eso sea un problema
Pedro Valencia
5
Desde la perspectiva de un purista de pruebas, tal vez sea un problema. Desde un punto de vista práctico, si foo()no hace nada más que llamar a la declaración de impresión, probablemente no sea un problema.
Alison R.
5

Basado en la respuesta de Rob Kennedy, escribí una versión basada en clases del administrador de contexto para almacenar la salida.

El uso es como:

with OutputBuffer() as bf:
    print('hello world')
assert bf.out == 'hello world\n'

Aquí está la implementación:

from io import StringIO
import sys


class OutputBuffer(object):

    def __init__(self):
        self.stdout = StringIO()
        self.stderr = StringIO()

    def __enter__(self):
        self.original_stdout, self.original_stderr = sys.stdout, sys.stderr
        sys.stdout, sys.stderr = self.stdout, self.stderr
        return self

    def __exit__(self, exception_type, exception, traceback):
        sys.stdout, sys.stderr = self.original_stdout, self.original_stderr

    @property
    def out(self):
        return self.stdout.getvalue()

    @property
    def err(self):
        return self.stderr.getvalue()
Hugo Mota
fuente
2

O considere usarlo pytest, tiene soporte incorporado para afirmar stdout y stderr. Ver documentos

def test_myoutput(capsys): # or use "capfd" for fd-level
    print("hello")
    captured = capsys.readouterr()
    assert captured.out == "hello\n"
    print("next")
    captured = capsys.readouterr()
    assert captured.out == "next\n"
Michel Samia
fuente
Buena. ¿Puede incluir un ejemplo mínimo, ya que los enlaces pueden desaparecer y el contenido puede cambiar?
KobeJohn
2

Tanto n611x007 y Noúmeno ya se ha sugerido el uso unittest.mock, pero esta respuesta se adapta Acumenus de mostrar cómo se puede envolver fácilmente unittest.TestCasemétodos para interactuar con un objeto de burla stdout.

import io
import unittest
import unittest.mock

msg = "Hello World!"


# function we will be testing
def foo():
    print(msg, end="")


# create a decorator which wraps a TestCase method and pass it a mocked
# stdout object
mock_stdout = unittest.mock.patch('sys.stdout', new_callable=io.StringIO)


class MyTests(unittest.TestCase):

    @mock_stdout
    def test_foo(self, stdout):
        # run the function whose output we want to test
        foo()
        # get its output from the mocked stdout
        actual = stdout.getvalue()
        expected = msg
        self.assertEqual(actual, expected)
rovyko
fuente
0

Sobre la base de todas las respuestas increíbles en este hilo, así es como lo resolví. Quería mantenerlo lo más stock posible. Aumenté el mecanismo de prueba unitaria usando setUp()para capturar sys.stdouty sys.stderr, agregué nuevas API de aserción para verificar los valores capturados con un valor esperado y luego restaurar sys.stdouty sys.stderren tearDown(). I did this to keep a similar unit test API as the built-inunittest API while still being able to unit test values printed tosys.stdout orsys.stderr`.

import io
import sys
import unittest


class TestStdout(unittest.TestCase):

    # before each test, capture the sys.stdout and sys.stderr
    def setUp(self):
        self.test_out = io.StringIO()
        self.test_err = io.StringIO()
        self.original_output = sys.stdout
        self.original_err = sys.stderr
        sys.stdout = self.test_out
        sys.stderr = self.test_err

    # restore sys.stdout and sys.stderr after each test
    def tearDown(self):
        sys.stdout = self.original_output
        sys.stderr = self.original_err

    # assert that sys.stdout would be equal to expected value
    def assertStdoutEquals(self, value):
        self.assertEqual(self.test_out.getvalue().strip(), value)

    # assert that sys.stdout would not be equal to expected value
    def assertStdoutNotEquals(self, value):
        self.assertNotEqual(self.test_out.getvalue().strip(), value)

    # assert that sys.stderr would be equal to expected value
    def assertStderrEquals(self, value):
        self.assertEqual(self.test_err.getvalue().strip(), value)

    # assert that sys.stderr would not be equal to expected value
    def assertStderrNotEquals(self, value):
        self.assertNotEqual(self.test_err.getvalue().strip(), value)

    # example of unit test that can capture the printed output
    def test_print_good(self):
        print("------")

        # use assertStdoutEquals(value) to test if your
        # printed value matches your expected `value`
        self.assertStdoutEquals("------")

    # fails the test, expected different from actual!
    def test_print_bad(self):
        print("@=@=")
        self.assertStdoutEquals("@-@-")


if __name__ == '__main__':
    unittest.main()

Cuando se ejecuta la prueba unitaria, la salida es:

$ python3 -m unittest -v tests/print_test.py
test_print_bad (tests.print_test.TestStdout) ... FAIL
test_print_good (tests.print_test.TestStdout) ... ok

======================================================================
FAIL: test_print_bad (tests.print_test.TestStdout)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tests/print_test.py", line 51, in test_print_bad
    self.assertStdoutEquals("@-@-")
  File "/tests/print_test.py", line 24, in assertStdoutEquals
    self.assertEqual(self.test_out.getvalue().strip(), value)
AssertionError: '@=@=' != '@-@-'
- @=@=
+ @-@-


----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)
llagas
fuente