Wie kann ich die Ausgabe mit nosetest / unittest in Python bestätigen?


114

Ich schreibe Tests für eine Funktion wie die nächste:

def foo():
    print 'hello world!'

Wenn ich diese Funktion testen möchte, sieht der Code folgendermaßen aus:

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

Wenn ich jedoch Nosetests mit dem Parameter -s ausführe, stürzt der Test ab. Wie kann ich die Ausgabe mit unittest oder Nasenmodul abfangen?


Antworten:


124

Ich benutze diesen Kontextmanager , um die Ausgabe zu erfassen. Es verwendet letztendlich die gleiche Technik wie einige der anderen Antworten, indem es vorübergehend ersetzt wird sys.stdout. Ich bevorzuge den Kontextmanager, weil er die gesamte Buchhaltung in einer einzigen Funktion zusammenfasst, sodass ich keinen Try-finally-Code neu schreiben muss und keine Setup- und Teardown-Funktionen nur dafür schreiben muss.

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

Verwenden Sie es so:

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

Da der ursprüngliche Ausgabezustand beim Verlassen des withBlocks wiederhergestellt wird , können wir außerdem einen zweiten Erfassungsblock in derselben Funktion wie den ersten einrichten, was mit Setup- und Teardown-Funktionen nicht möglich ist und beim Schreiben von try-finally wortreich wird blockiert manuell. Diese Fähigkeit erwies sich als nützlich, wenn das Ziel eines Tests darin bestand, die Ergebnisse zweier Funktionen relativ zueinander und nicht mit einem vorberechneten Wert zu vergleichen.


Das hat bei pep8radius sehr gut funktioniert . Vor kurzem hatte ich dies jedoch erneut verwendet und beim Drucken die folgende Fehlermeldung erhalten TypeError: unicode argument expected, got 'str'(der an print übergebene Typ (str / unicode) ist irrelevant).
Andy Hayden

9
Hmmm es kann sein, dass wir in Python 2 wollen from io import BytesIO as StringIOund in Python 3 nur from io import StringIO. Schien das Problem in meinen Tests zu beheben, denke ich.
Andy Hayden

4
Ooop, nur zum Schluss, entschuldige mich für so viele Nachrichten. Nur um dies zu verdeutlichen: python3 benutze io.StringIO, python 2 benutze StringIO.StringIO! Danke noch einmal!
Andy Hayden

Warum rufen alle Beispiele hier strip()die unicodeRückkehr von auf StringIO.getvalue()?
Palimondo

1
Nein, @Vedran. Dies setzt voraus, dass der zugehörige Name erneut gebunden wird sys. Mit Ihrer Importanweisung erstellen Sie eine lokale Variable mit dem Namen stderr, die eine Kopie des Werts in erhalten hat sys.stderr. Änderungen an einem werden nicht im anderen wiedergegeben.
Rob Kennedy

60

Wenn Sie dies wirklich tun möchten, können Sie sys.stdout für die Dauer des Tests neu zuweisen.

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

Wenn ich diesen Code schreiben würde, würde ich outder fooFunktion lieber einen optionalen Parameter übergeben .

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

Dann ist der Test viel einfacher:

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
Hinweis: Unter Python 3.x StringIOmuss die Klasse jetzt aus dem ioModul importiert werden. from io import StringIOfunktioniert in Python 2.6+.
Bryan P

2
Wenn Sie from io import StringIOin Python 2 verwenden, erhalten Sie TypeError: unicode argument expected, got 'str'beim Drucken eine.
Matiasg

9
Kurze Anmerkung: In Python 3.4 können Sie die Verwendung contextlib.redirect_stdout Kontext - Manager dies in einer Art und Weise zu tun , die Ausnahme ist sicher:with redirect_stdout(out):
Lucretiel

2
Sie müssen nicht tun saved_stdout = sys.stdout, Sie haben immer einen magischen Hinweis darauf sys.__stdout__, z. B. brauchen Sie nur sys.stdout = sys.__stdout__in Ihrer Bereinigung.
ThorSummoner

@ThorSummoner Danke, dies hat nur einige meiner Tests vereinfacht ... für das Tauchen, von dem ich sehe, dass du die Hauptrolle gespielt hast ... kleine Welt!
Jonathon Reinhart

48

Seit Version 2.7 müssen Sie keine Neuzuweisung mehr vornehmen sys.stdout. Dies wird über das bufferFlag bereitgestellt . Darüber hinaus ist dies das Standardverhalten von Nosetest.

Hier ist ein Beispiel, das im nicht gepufferten Kontext fehlschlägt:

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

Sie können einstellen , Puffer durch unit2Befehlszeilenparameter -b, --bufferoder in unittest.mainOptionen. Das Gegenteil wird durch nosetestFlagge erreicht --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)

Beachten Sie, dass dies mit interagiert --nocapture; Insbesondere wenn dieses Flag gesetzt ist, wird der gepufferte Modus deaktiviert. Sie haben also die Möglichkeit, entweder die Ausgabe auf dem Terminal zu sehen oder zu testen, ob die Ausgabe wie erwartet ist.
Ijoseph

1
Ist es möglich, dies für jeden Test ein- und auszuschalten, da dies das Debuggen bei Verwendung von ipdb.set_trace () sehr schwierig macht?
Lqueryvg

33

Viele dieser Antworten sind für mich fehlgeschlagen, weil Sie dies from StringIO import StringIOin Python 3 nicht können . Hier ist ein minimaler Arbeitsausschnitt, der auf dem Kommentar von @ naxa und dem Python-Kochbuch basiert.

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
Ich liebe dieses für Python 3, es ist sauber!
Sylhare

1
Dies war die einzige Lösung auf dieser Seite, die für mich funktioniert hat! Danke dir.
Justin Eyster

24

In Python 3.5 können Sie contextlib.redirect_stdout()und verwenden StringIO(). Hier ist die Änderung an Ihrem 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!'

Gute Antwort! Laut Dokumentation wurde dies in Python 3.4 hinzugefügt.
Hypercube

Es ist 3.4 für redirect_stdout und 3.5 für redirect_stderr. Vielleicht entstand dort die Verwirrung!
Rbennell

redirect_stdout()und redirect_stderr()geben ihr Eingabeargument zurück. Also, with contextlib.redirect_stdout(StringIO()) as temp_stdout:gibt Ihnen alles in einer Zeile. Getestet mit 3.7.1.
Adrian W

17

Ich lerne gerade erst Python und habe mit einem ähnlichen Problem wie oben mit Unit-Tests für Methoden mit Ausgabe zu kämpfen. Mein bestehender Unit-Test für das oben genannte foo-Modul sieht folgendermaßen aus:

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
Vielleicht möchten Sie einen machen sys.stdout.getvalue().strip()und nicht betrügen im Vergleich zu \n:)
Silviu

Das StringIO-Modul ist veraltet. Stattdessenfrom io import StringIO
Edwarric

10

Das Schreiben von Tests zeigt uns oft, wie wir unseren Code besser schreiben können. Ähnlich wie bei Shane möchte ich noch eine andere Sichtweise vorschlagen. Möchten Sie wirklich behaupten, dass Ihr Programm eine bestimmte Zeichenfolge ausgegeben hat oder nur eine bestimmte Zeichenfolge für die Ausgabe erstellt hat? Dies wird einfacher zu testen, da wir wahrscheinlich davon ausgehen können, dass die Python- printAnweisung ihre Aufgabe korrekt erfüllt.

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

Dann ist Ihr Test sehr einfach:

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

Wenn Sie wirklich die tatsächliche Ausgabe Ihres Programms testen müssen, können Sie dies natürlich ignorieren. :) :)


1
aber in diesem Fall wird foo nicht getestet ... vielleicht ist das ein Problem
Pedro Valencia

5
Aus der Sicht eines Testpuristen ist es vielleicht ein Problem. Aus praktischer Sicht ist foo()es wahrscheinlich kein Problem , wenn Sie nichts anderes tun, als die print-Anweisung aufzurufen .
Alison R.

5

Basierend auf Rob Kennedys Antwort habe ich eine klassenbasierte Version des Kontextmanagers geschrieben, um die Ausgabe zu puffern.

Verwendung ist wie:

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

Hier ist die Implementierung:

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

Oder erwägen Sie die Verwendung pytest, es verfügt über eine integrierte Unterstützung für die Bestätigung von stdout und stderr. Siehe Dokumente

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"

Gut. Können Sie ein minimales Beispiel angeben, da Links verschwinden und sich der Inhalt ändern kann?
KobeJohn

2

Sowohl n611x007 als auch Noumenon schlugen bereits die Verwendung vor unittest.mock, aber diese Antwort passt Acumenus an, um zu zeigen, wie Sie unittest.TestCaseMethoden zur Interaktion mit einem verspotteten einfach umschließen können 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

Aufbauend auf all den fantastischen Antworten in diesem Thread habe ich es so gelöst. Ich wollte es so gut wie möglich auf Lager halten. Ich ergänzte den Unit - Test - Mechanismus setUp()zu Erfassung sys.stdoutund sys.stderrfügte neuer assert APIs den erfassten Wert mit einem erwarteten Wert zu prüfen und dann wiederherstellen sys.stdoutund sys.stderrauf 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()

Wenn der Komponententest ausgeführt wird, lautet die Ausgabe:

$ 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)
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.