Як стверджувати вихід із нос-тестом / unittest в python?


114

Я пишу тести на функцію, як наступна:

def foo():
    print 'hello world!'

Отже, коли я хочу перевірити цю функцію, код буде таким:

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

Але якщо я запускаю ностестери з параметром -s, тест завершується. Як я можу зафіксувати вихід за допомогою модуля unittest або носа?


Відповіді:


124

Я використовую цей контекстний менеджер для збору результатів. Зрештою, він використовує ту саму техніку, що і деякі інші відповіді тимчасово замінюючи sys.stdout. Я віддаю перевагу контекстному менеджеру, тому що він об'єднує всі бухгалтерії в одну функцію, тому мені не потрібно переписувати будь-який код пробного останнього, і мені не потрібно писати функції налаштування та відключення саме для цього.

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

Використовуйте його так:

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

Крім того, оскільки вихідний стан виходу відновлюється після виходу з withблоку, ми можемо встановити другий блок захоплення в тій же функції, що і перший, що неможливо за допомогою функцій настройки та вилучення, і отримує слово під час написання спробувати блоки вручну. Ця здатність виявилася корисною, коли метою тесту було порівняння результатів двох функцій відносно один одного, а не якогось попереднього обчисленого значення.


Це справді добре працювало для мене в pep8radius . Однак останнім часом я знову використав це і отримав таку помилку під час друку TypeError: unicode argument expected, got 'str'(тип переданого друку (str / unicode) не має значення).
Енді Хайден

9
Гммм, може бути, що в python 2 ми хочемо, from io import BytesIO as StringIOа в python 3 просто from io import StringIO. Я, здається, вирішив проблему в своїх тестах, я думаю.
Енді Хайден

4
На жаль, закінчую, вибачте за стільки повідомлень. Просто для уточнення для людей, які знаходять це: python3 використовуйте io.StringIO, python 2 використовуйте StringIO.StringIO! Знову дякую!
Енді Хайден

Чому все тут приклади виклику strip()на unicodeповернувся з StringIO.getvalue()?
Палімондо

1
Ні, @Vedran. Це покладається на перезаписування імені, яке належить sys. За допомогою імпорту імпорту ви створюєте локальну змінну на ім’я, stderrяка отримала копію значення в sys.stderr. Зміни в одній не відображаються в іншому.
Роб Кеннеді

60

Якщо ви дійсно хочете це зробити, ви можете перепризначити sys.stdout протягом тривалості тесту.

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

Однак, якби я писав цей код, я вважаю за краще передавати додатковий outпараметр fooфункції.

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

Тоді тест набагато простіший:

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
Примітка: Під python 3.x StringIOклас зараз потрібно імпортувати з ioмодуля. from io import StringIOпрацює в python 2.6+.
Брайан П

2
Якщо ви використовуєте from io import StringIOв python 2, ви отримуєте TypeError: unicode argument expected, got 'str'друк під час друку.
matiasg

9
Коротка примітка: В Пітоні 3.4, ви можете використовувати contextlib.redirect_stdout менеджер контексту , щоб зробити це таким чином , що є безпечним винятком:with redirect_stdout(out):
Lucretiel

2
Вам не потрібно цього робити saved_stdout = sys.stdout, у вас завжди є чарівна відповідь на це питання sys.__stdout__, наприклад, вам потрібно лише sys.stdout = sys.__stdout__в прибиранні.
ThorSummoner

@ThorSummoner Спасибі, це просто спростило деякі мої тести ... для аквалангу, який, я бачу, ви зіграли .... маленький світ!
Джонатан Райнхарт

48

Оскільки у версії 2.7 переназначення більше не потрібно sys.stdout, це надається через bufferпрапор . Більше того, це типова поведінка носа.

Ось приклад провалу в небуферному контексті:

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

Ви можете встановити буфер через unit2прапор командного рядка -b, --bufferабо в unittest.mainопції. Протилежне досягається через nosetestпрапор --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)

Зауважте, що це взаємодіє з --nocapture; зокрема, якщо цей прапор встановлено, режим буферизації буде відключений. Таким чином, у вас є можливість або бачити вихід на терміналі, або мати можливість перевірити, чи є вихід таким, як очікувалося.
ijoseph

1
Чи можна вмикати та вимикати це для кожного тесту, оскільки це робить налагодження дуже важким при використанні чогось типу ipdb.set_trace ()?
Lqueryvg

33

Багато цих відповідей мені не вдалося, оскільки ви не можете from StringIO import StringIOв Python 3. Ось мінімальний робочий фрагмент, заснований на коментарі @ naxa та кулінарній книзі 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
Мені подобається цей Python 3, він чистий!
Sylhare

1
Це єдине рішення на цій сторінці, яке працювало на мене! Дякую.
Джастін Ейстер,

24

У python 3.5 можна використовувати contextlib.redirect_stdout()і StringIO(). Ось модифікація вашого коду

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

Чудова відповідь! Відповідно до документації, це було додано в Python 3.4.
Hypercube

Це 3,4 для redirect_stdout і 3,5 для redirect_stderr. можливо, саме тут виник плутанина!
rbennell

redirect_stdout()і redirect_stderr()повернути їх вхідний аргумент. Отже, with contextlib.redirect_stdout(StringIO()) as temp_stdout:дає вам все в один рядок. Перевірено з 3.7.1.
Адріан Ш

17

Я лише вивчав Python і опинився, що бореться з подібною проблемою, як описано вище, з одиничними тестами для методів з виходом. Мій тест на проходження модуля foo вище був закінчений таким чином:

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
Можливо, ви хочете зробити, sys.stdout.getvalue().strip()а не обманювати порівняння з \n:)
Silviu

Модуль StringIO застарілий. Натомістьfrom io import StringIO
Едваррік

10

Написання тестів часто показує нам кращий спосіб написати наш код. Подібно до відповіді Шейна, я хотів би запропонувати ще один спосіб поглянути на це. Ви дійсно хочете стверджувати, що ваша програма виводила певну рядок або просто, що вона сконструювала певну рядок для виведення? Це стає простішим для перевірки, оскільки ми можемо припустити, що printоператор Python виконує свою роботу правильно.

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

Тоді ваш тест дуже простий:

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

Звичайно, якщо вам справді потрібно перевірити фактичний результат програми, тоді не соромтеся ігнорувати. :)


1
але в цьому випадку foo не буде перевірений ... можливо, це проблема
Педро Валенсія

5
З точки зору пуристів тестування, можливо, це проблема. З практичної точки зору, якщо foo()нічого не виходить, окрім заклику до друку, це, мабуть, не проблема.
Елісон Р.

5

Спираючись на відповідь Роб Кеннеді, я написав на основі класу версію контекстного менеджера для буфера виводу.

Використання виглядає так:

with OutputBuffer() as bf:
    print('hello world')
assert bf.out == 'hello world\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()

2

Або подумайте про використання pytest, вона має вбудовану підтримку для ствердження stdout та stderr. Див. Документи

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"

Хороший. Чи можете ви включити мінімальний приклад, оскільки посилання можуть зникати, а вміст може змінюватися?
KobeJohn

2

Обидва n611x007 і Ноумен вже запропонували використовувати unittest.mock, але ця відповідь адаптує Акаменус - х , щоб показати , як можна легко обернути unittest.TestCaseметоди , щоб взаємодіяти з знущався 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

Спираючись на всі приголомшливі відповіді в цій нитці, ось як я це вирішив. Я хотів утримати його якнайбільше запасів. Я доповнив механізм тестування одиниць, використовуючи setUp()захоплення, sys.stdoutі sys.stderrдодав нові API затвердження, щоб перевірити захоплені значення на очікуване значення, а потім відновити sys.stdoutі sys.stderrпісля 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 sys.stderr` or.

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

Коли тест одиниці виконується, вихід:

$ 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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.