Как подтвердить вывод с помощью nodetest / unittest в Python?


115

Я пишу тесты для такой функции:

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 или носового модуля?


Ответы:


126

Я использую этот диспетчер контекста для захвата вывода. В конечном итоге он использует ту же технику, что и некоторые другие ответы, путем временной замены 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блока, мы можем настроить второй блок захвата в той же функции, что и первый, что невозможно с использованием функций настройки и разрыва, и становится многословным при написании try-finally блоки вручную. Эта возможность пригодилась, когда целью теста было сравнение результатов двух функций относительно друг друга, а не с некоторым заранее вычисленным значением.


Это очень хорошо сработало для меня в 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()?
Palimondo

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+.
Bryan P

2
Если вы используете from io import StringIOPython 2, TypeError: unicode argument expected, got 'str'при печати вы получите .
matiasg

9
Краткое примечание: в python 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 05

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 07

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 07

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-интерфейсы assert, чтобы проверять захваченные значения на соответствие ожидаемому значению, а затем восстанавливать 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 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()

Когда запускается модульный тест, на выходе получается:

$ 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.