Comment affirmer la sortie avec nosetest / unittest en python?


114

J'écris des tests pour une fonction comme la suivante:

def foo():
    print 'hello world!'

Donc, quand je veux tester cette fonction, le code sera comme ceci:

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

Mais si j'exécute nosetests avec le paramètre -s, le test plante. Comment puis-je capturer la sortie avec unittest ou un module nasal?


Réponses:


125

J'utilise ce gestionnaire de contexte pour capturer la sortie. Il utilise finalement la même technique que certaines des autres réponses en les remplaçant temporairement sys.stdout. Je préfère le gestionnaire de contexte car il englobe toute la comptabilité dans une seule fonction, donc je n'ai pas à réécrire de code d'essai, et je n'ai pas à écrire des fonctions de configuration et de démontage juste pour cela.

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

Utilisez-le comme ceci:

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

De plus, comme l'état de sortie d'origine est restauré à la sortie du withbloc, nous pouvons configurer un deuxième bloc de capture dans la même fonction que le premier, ce qui n'est pas possible en utilisant les fonctions de configuration et de démontage, et devient verbeux lors de l'écriture try-finally bloque manuellement. Cette capacité était utile lorsque l'objectif d'un test était de comparer les résultats de deux fonctions l'une par rapport à l'autre plutôt qu'avec une valeur précalculée.


Cela a très bien fonctionné pour moi dans pep8radius . Récemment, cependant, je l'ai utilisé à nouveau et j'ai obtenu l'erreur suivante lors de l'impression TypeError: unicode argument expected, got 'str'(le type passé à imprimer (str / unicode) n'est pas pertinent).
Andy Hayden

9
Hmmm, il se peut qu'en python 2 nous voulons from io import BytesIO as StringIOet en python 3 juste from io import StringIO. Semblait résoudre le problème dans mes tests, je pense.
Andy Hayden

4
Ooop, juste pour finir, excuses pour tant de messages. Juste pour clarifier pour les gens qui trouvent ceci: python3 utilise io.StringIO, python 2 utilise StringIO.StringIO! Merci encore!
Andy Hayden

Pourquoi tous les exemples ici font-ils appel strip()au unicoderetour de StringIO.getvalue()?
Palimondo

1
Non, @Vedran. Cela repose sur la reliure du nom auquel appartient sys. Avec votre instruction d'importation, vous créez une variable locale nommée stderrqui a reçu une copie de la valeur dans sys.stderr. Les modifications apportées à l'un ne sont pas reflétées dans l'autre.
Rob Kennedy

60

Si vous voulez vraiment faire cela, vous pouvez réaffecter sys.stdout pour la durée du test.

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

Si j'écrivais ce code, cependant, je préférerais passer un outparamètre facultatif à la foofonction.

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

Ensuite, le test est beaucoup plus 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!'

11
Remarque: sous python 3.x, la StringIOclasse doit maintenant être importée du iomodule. from io import StringIOfonctionne en python 2.6+.
Bryan P

2
Si vous utilisez from io import StringIOen python 2, vous obtenez un TypeError: unicode argument expected, got 'str'lors de l'impression.
matiasg

9
Note rapide: Dans python 3.4, vous pouvez utiliser le gestionnaire de contexte contextlib.redirect_stdout pour faire cela d'une manière qui est exceptionnellement sûre:with redirect_stdout(out):
Lucretiel

2
Vous n'avez pas besoin de faire saved_stdout = sys.stdout, vous avez toujours une référence magique à cela sys.__stdout__, par exemple, vous n'en avez besoin que sys.stdout = sys.__stdout__dans votre nettoyage.
ThorSummoner

@ThorSummoner Merci, cela a simplifié certains de mes tests ... pour la plongée sous-marine dont je vois que vous avez joué ... petit monde!
Jonathon Reinhart

48

Depuis la version 2.7, vous n'avez plus besoin de réassigner sys.stdout, cela est fourni via bufferflag . De plus, c'est le comportement par défaut de nosetest.

Voici un exemple d'échec dans un contexte non tamponné:

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

Vous pouvez définir par buffer unit2indicateur de ligne de commande -b, --bufferou dans les unittest.mainoptions. Le contraire est obtenu grâce au nosetestdrapeau --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)

Notez que cela interagit avec --nocapture; en particulier, si cet indicateur est défini, le mode tampon sera désactivé. Vous avez donc la possibilité soit de voir la sortie sur le terminal, soit de pouvoir tester que la sortie est comme prévu.
ijoseph le

1
Est-il possible d'activer et de désactiver cela pour chaque test, car cela rend le débogage très difficile lors de l'utilisation de quelque chose comme ipdb.set_trace ()?
Lqueryvg

33

Beaucoup de ces réponses ont échoué pour moi parce que vous ne pouvez pas from StringIO import StringIOen Python 3. Voici un extrait de code de travail minimum basé sur le commentaire de @ naxa et le livre de recettes 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')

3
J'adore celui-ci pour Python 3, il est propre!
Sylhare

1
C'était la seule solution sur cette page qui a fonctionné pour moi! Je vous remercie.
Justin Eyster le

24

En python 3.5, vous pouvez utiliser contextlib.redirect_stdout()et StringIO(). Voici la modification de votre code

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

Très bonne réponse! Selon la documentation, cela a été ajouté dans Python 3.4.
Hypercube

C'est 3.4 pour redirect_stdout et 3.5 pour redirect_stderr. c'est peut-être là que la confusion est née!
rbennell

redirect_stdout()et redirect_stderr()renvoyer leur argument d'entrée. Donc, with contextlib.redirect_stdout(StringIO()) as temp_stdout:vous donne tout en une seule ligne. Testé avec 3.7.1.
Adrian W le

17

J'apprends juste Python et je me suis retrouvé aux prises avec un problème similaire à celui ci-dessus avec des tests unitaires pour les méthodes avec sortie. Mon test unitaire de réussite pour le module foo ci-dessus a fini par ressembler à ceci:

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

5
Vous voudrez peut-être faire une sys.stdout.getvalue().strip()comparaison et ne pas tricher avec \n:)
Silviu

Le module StringIO est obsolète. Au lieu de celafrom io import StringIO
Edwarric

10

L'écriture de tests nous montre souvent une meilleure façon d'écrire notre code. Semblable à la réponse de Shane, j'aimerais suggérer une autre façon de voir cela. Voulez-vous vraiment affirmer que votre programme a produit une certaine chaîne, ou simplement qu'il a construit une certaine chaîne pour la sortie? Cela devient plus facile à tester, car nous pouvons probablement supposer que l' printinstruction Python fait correctement son travail.

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

Ensuite, votre test est très simple:

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

Bien sûr, si vous avez vraiment besoin de tester la sortie réelle de votre programme, n'hésitez pas à ne pas en tenir compte. :)


1
mais dans ce cas, foo ne sera pas testé ... c'est peut-être un problème
Pedro Valencia

5
Du point de vue d'un puriste du test, c'est peut-être un problème. D'un point de vue pratique, si foo()cela ne fait rien d'autre que d'appeler l'instruction print, ce n'est probablement pas un problème.
Alison R.

5

Sur la base de la réponse de Rob Kennedy, j'ai écrit une version basée sur les classes du gestionnaire de contexte pour tamponner la sortie.

L'utilisation est comme:

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

Voici la mise en œuvre:

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

2

Ou pensez à l'utiliser pytest, il a un support intégré pour l'affirmation de stdout et stderr. Voir la documentation

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"

Bon. Pouvez-vous inclure un exemple minimal puisque les liens peuvent disparaître et le contenu peut changer?
KobeJohn

2

Les deux n611x007 et Noumenon déjà suggéré d' utiliser unittest.mock, mais cette réponse adapte Acumenus de montrer comment vous pouvez facilement envelopper des unittest.TestCaseméthodes pour interagir avec un moqué 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)

0

En m'appuyant sur toutes les réponses impressionnantes de ce fil, voici comment je l'ai résolu. Je voulais le garder aussi stock que possible. J'ai augmenté le mécanisme de test unitaire en utilisant setUp()pour capturer sys.stdoutet sys.stderrajouté de nouvelles API d'assert pour vérifier les valeurs capturées par rapport à une valeur attendue, puis restaurer sys.stdoutet sys.stderrsur 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()

Lorsque le test unitaire est exécuté, la sortie est:

$ 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)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.