Jak zapewnić dane wyjściowe za pomocą nosetest / unittest w Pythonie?


114

Piszę testy dla funkcji takiej jak następna:

def foo():
    print 'hello world!'

Więc kiedy chcę przetestować tę funkcję, kod będzie wyglądał tak:

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

Ale jeśli uruchomię testy nosetesty z parametrem -s, test zawiesza się. Jak mogę złapać wyjście za pomocą modułu unittest lub nosowego?


Odpowiedzi:


125

Używam tego menedżera kontekstu do przechwytywania danych wyjściowych. Ostatecznie wykorzystuje tę samą technikę, co niektóre inne odpowiedzi, tymczasowo zastępując sys.stdout. Wolę menedżera kontekstu, ponieważ zawija on całą księgowość w jedną funkcję, więc nie muszę ponownie pisać żadnego kodu próbnego i nie muszę pisać funkcji konfiguracji i dezaktywacji tylko w tym celu.

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

Użyj tego w ten sposób:

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

Ponadto, ponieważ pierwotny stan wyjściowy jest przywracany po wyjściu z withbloku, możemy ustawić drugi blok przechwytywania w tej samej funkcji co pierwszy, co nie jest możliwe przy użyciu funkcji konfiguracji i porzucania, i staje się rozwlekłe podczas pisania try-last blokuje się ręcznie. Zdolność ta przydała się, gdy celem testu było porównanie wyników dwóch funkcji względem siebie, a nie z jakąś wstępnie obliczoną wartością.


To zadziałało dla mnie naprawdę dobrze w pep8radius . Ostatnio jednak użyłem tego ponownie i podczas drukowania pojawił się następujący błąd TypeError: unicode argument expected, got 'str'(typ przekazany do print (str / unicode) jest nieistotny).
Andy Hayden,

9
Hmmm może być tak, że w Pythonie 2 chcemy, from io import BytesIO as StringIOaw Pythonie 3 po prostu from io import StringIO. Wydaje mi się, że rozwiązało to problem w moich testach.
Andy Hayden,

4
Ups, żeby skończyć, przepraszam za tak wiele wiadomości. Dla wyjaśnienia dla osób, które to znajdą: python3 używa io.StringIO, python 2 używa StringIO.StringIO! Dzięki jeszcze raz!
Andy Hayden,

Dlaczego wszystkie przykłady tutaj wzywają strip()do unicodepowrotu StringIO.getvalue()?
Palimondo

1
Nie, @Vedran. Polega to na ponownym powiązaniu nazwy, do której należy sys. Za pomocą instrukcji importu tworzysz zmienną lokalną o nazwie, stderrktóra otrzymała kopię wartości w formacie sys.stderr. Zmiany jednego nie są odzwierciedlane w drugim.
Rob Kennedy

60

Jeśli naprawdę chcesz to zrobić, możesz ponownie przypisać sys.stdout na czas trwania testu.

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

Gdybym jednak pisał ten kod, wolałbym przekazać outdo foofunkcji opcjonalny parametr .

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

Wtedy test jest znacznie prostszy:

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
Uwaga: W Pythonie 3.x StringIOklasa musi teraz zostać zaimportowana z iomodułu. from io import StringIOdziała w Pythonie 2.6+.
Bryan P

2
Jeśli używasz from io import StringIOw Pythonie 2, TypeError: unicode argument expected, got 'str'podczas drukowania otrzymasz .
matiasg

9
Krótka uwaga: w Pythonie 3.4 możesz użyć menedżera kontekstu contextlib.redirect_stdout , aby zrobić to w sposób bezpieczny dla wyjątków:with redirect_stdout(out):
Lucretiel

2
Nie musisz tego robić saved_stdout = sys.stdout, zawsze masz do tego magiczne odniesienie sys.__stdout__, np. Potrzebujesz tylko sys.stdout = sys.__stdout__do sprzątania.
ThorSummoner

@ThorSummoner Dzięki, to właśnie uprościło niektóre z moich testów ... w przypadku nurkowania z akwalungiem, w którym wystąpiłeś ... mały świat!
Jonathon Reinhart

48

Od wersji 2.7 nie ma już potrzeby ponownego przypisywania sys.stdout, zapewnia to bufferflaga . Ponadto jest to domyślne zachowanie nosetest.

Oto przykład niepowodzenia w kontekście niebuforowanym:

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

Można ustawić bufor poprzez unit2flagi linii poleceń -b, --bufferlub w unittest.mainopcji. Odwrotność osiąga się za pomocą nosetestflagi --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)

Zauważ, że współdziała z --nocapture; w szczególności, jeśli ta flaga jest ustawiona, tryb buforowany zostanie wyłączony. Masz więc możliwość zobaczenia wyjścia na terminalu lub sprawdzenia, czy wyjście jest zgodne z oczekiwaniami.
ijoseph

1
Czy można to włączać i wyłączać dla każdego testu, ponieważ bardzo utrudnia to debugowanie przy użyciu czegoś takiego jak ipdb.set_trace ()?
Lqueryvg

33

Wiele z tych odpowiedzi zawiodło, ponieważ nie możesz tego zrobić from StringIO import StringIOw Pythonie 3. Oto minimalny działający fragment oparty na komentarzu @ naxa i książce kucharskiej Pythona.

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
Uwielbiam ten dla Pythona 3, jest czysty!
Sylhare

1
To było jedyne rozwiązanie na tej stronie, które działało dla mnie! Dziękuję Ci.
Justin Eyster

24

W Pythonie 3.5 możesz używać contextlib.redirect_stdout()i StringIO(). Oto modyfikacja Twojego kodu

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

Świetna odpowiedź! Zgodnie z dokumentacją zostało to dodane w Pythonie 3.4.
Hypercube

To 3.4 dla redirect_stdout i 3.5 dla redirect_stderr. może właśnie tam powstało zamieszanie!
rbennell,

redirect_stdout()i redirect_stderr()zwraca ich argument wejściowy. Więc with contextlib.redirect_stdout(StringIO()) as temp_stdout:daje wszystko w jednej linii. Przetestowano pod 3.7.1.
Adrian W

17

Dopiero uczę się języka Python i zmagam się z podobnym problemem, jak ten powyżej, z testami jednostkowymi dla metod z danymi wyjściowymi. Mój pozytywny test jednostkowy dla modułu foo powyżej zakończył się następująco:

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
Możesz chcieć zrobić sys.stdout.getvalue().strip()i nie oszukiwać w porównaniu z \n:)
Silviu

Moduł StringIO jest przestarzały. Zamiast tegofrom io import StringIO
Edwarric

10

Pisanie testów często pokazuje nam lepszy sposób pisania naszego kodu. Podobnie jak w przypadku odpowiedzi Shane'a, chciałbym zasugerować jeszcze inny sposób spojrzenia na to. Czy naprawdę chcesz zapewnić, że twój program wypisał określony ciąg, czy po prostu skonstruował określony ciąg na wyjście? Staje się to łatwiejsze do przetestowania, ponieważ prawdopodobnie możemy założyć, że instrukcja Pythona printdziała poprawnie.

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

Wtedy twój test jest bardzo prosty:

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

Oczywiście, jeśli naprawdę potrzebujesz przetestować rzeczywiste wyniki programu, możesz to zignorować. :)


1
ale w tym przypadku foo nie będzie testowane ... może to jest problem
Pedro Valencia

5
Z punktu widzenia testującego purysty, być może jest to problem. Z praktycznego punktu widzenia, jeśli foo()nie robi nic poza wywołaniem instrukcji print, prawdopodobnie nie stanowi to problemu.
Alison R.

5

Na podstawie odpowiedzi Roba Kennedy'ego napisałem wersję menedżera kontekstu opartą na klasach, aby buforować dane wyjściowe.

Sposób użycia jest następujący:

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

Oto implementacja:

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

Lub rozważ użycie pytest, ma wbudowaną obsługę potwierdzania stdout i stderr. Zobacz dokumentację

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"

Dobry. Czy możesz podać minimalny przykład, ponieważ linki mogą zniknąć, a treść może się zmienić?
KobeJohn,

2

Zarówno n611x007, jak i Noumenon już sugerowały użycie unittest.mock, ale ta odpowiedź dostosowuje Acumenus, aby pokazać, jak łatwo można opakować unittest.TestCasemetody do interakcji z wyśmiewanym 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

Opierając się na wszystkich niesamowitych odpowiedziach w tym wątku, oto jak to rozwiązałem. Chciałem, aby był jak najbardziej zapasowy. Rozszerzyłem mechanizm testów jednostkowych za pomocą setUp()przechwytywania sys.stdouti sys.stderrdodałem nowe interfejsy API potwierdzania, aby sprawdzić przechwycone wartości względem oczekiwanej wartości, a następnie przywrócić sys.stdouti sys.stderrpo 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()

Po uruchomieniu testu jednostkowego dane wyjściowe to:

$ 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)
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.