Как утверждать вывод с помощью тестов на нос / юнит-тест в Python?

Я пишу тесты для функции, подобной следующей:

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

13 ответов

Я использую этот менеджер контекста для захвата вывода. В конечном итоге он использует ту же технику, что и некоторые другие ответы, временно заменяя sys.stdout, Я предпочитаю менеджер контекста, потому что он объединяет всю бухгалтерию в одну функцию, поэтому мне не нужно переписывать какой-либо код try-finally, и мне не нужно писать функции setup и teardown только для этого.

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

Если вы действительно хотите это сделать, вы можете переназначить 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!'

Начиная с версии 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)

Многие из этих ответов не удалось для меня, потому что вы не можете 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')

В 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 и столкнулся с проблемой, аналогичной приведенной выше, с модульными тестами для методов с выводом. Мой тестовый модуль для модуля 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')

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

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

Тогда ваш тест очень прост:

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

Конечно, если вам действительно нужно проверить фактический вывод вашей программы, не стесняйтесь игнорировать.:)

Оба n611x007 и Ноумен уже предложили использоватьunittest.mock, но этот ответ адаптирует Acumenus, чтобы показать, как можно легко обернуть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)

Основываясь на ответе Роба Кеннеди, я написал классную версию диспетчера контекста для буферизации вывода.

Использование как:

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

Или подумайте об использовании 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"

Unittest теперь поставляется с диспетчером контекста (Python 3.7, но, возможно, и более ранние версии). Вы можете просто сделать это:

      # example.py

import logging

def method_with_logging():
    logging.info("Hello, World!")

Затем в вашем модульном тесте:

      # test.py

from unittest import TestCase
from example import method_with_logging

class TestExample(TestCase):
    def test_logging(self):
        with self.assertLogs() as captured:
            method_with_logging()
        self.assertEqual(len(captured.records), 1) # check that there is only one log message
        self.assertEqual(captured.records[0].getMessage(), "Hello, World!") # and it is the proper one

Взято с https://pythonin1minute.com/how-to-test-logging-in-python/

Мне нравится простой [Ответ][1] Соренса на вопрос и пример кода, особенно потому, что я не знаком с новыми функциями, такими как patch/mock. sorens не предлагал способ сделать настраиваемые методы утверждения класса TestStdIO в примере кода повторно используемыми, не прибегая к вырезанию / вставке, поэтому я решил сделать TestStdIO "смешанным" классом, определенным в его собственном модуле (teststdoutmethods.py в следующий пример). Так как обычно unittest.TestCase-provided утверждают ссылки метод, используемый в TestStdIO также будут доступны в классе тестового примера, я удалил импорт UnitTest строки из его примеров кода, а также вывод TestStdIO из unittest.TestCase в объявлении класса, т. е.

       import io
import sys

class TestStdIO(object):
    def setUp(self):
        ...

В противном случае код TestStdIO будет такой же, как версия sorens без двух примеров использования в конце. Я использовал эту версию TestStdIO класса mixin в некоторых простых тестовых примерах unittest класса в одной из основных текстовых игр в гл. 2 книги Кинсли и МакГугана " Начало программирования игр на Python с помощью PyGame", например

       import unittest
from teststdoutmethods import TestStdIO   # sorens' TestStdIO as a mixin.
from tank import Tank  # From Beginning Python Game Programming with PyGame.

class Test_Tank_fire(TestStdIO, unittest.TestCase):   # Note multiple inheritance.

    def test_Tank_fire_wAmmo(self):
        oTank1 = Tank('Bill', 5, 100)
        oTank2 = Tank('Jim', 5, 100)

        self.setUp()
        oTank1.fire_at(oTank2)

        self.assertStdoutEquals("Bill fires on Jim\nJim is hit!")
        self.assertEqual(str(oTank1), 'Bill (100 Armor, 4 Ammo)', 'fire_at shooter attribute results incorrect')
        self.assertTrue(str(oTank2) == 'Jim (80 Armor, 5 Ammo)', 'fire_at target attribute results incorrect')

        self.tearDown()

    def test_Tank_fire_woAmmo(self):
        oTank1 = Tank('Bill', 5, 100)
        oTank2 = Tank('Jim', 5, 100)

        # Use up 5 allotted shots.
        for n in range(5):
            oTank1.fire_at(oTank2)

        self.setUp()
        # Try one more.
        oTank1.fire_at(oTank2)

        self.assertStdoutEquals("Bill has no shells!")

        self.tearDown()
    
    def test_Tank_explode(self):
        oTank1 = Tank('Bill', 5, 100)
        oTank2 = Tank('Jim', 5, 100)

        # Use up 4 shots.
        for n in range(4):
            oTank1.fire_at(oTank2)

        self.setUp()
        # Fifth shot should finish the target.
        oTank1.fire_at(oTank2)

        self.assertStdoutEquals("Bill fires on Jim\nJim is hit!\nJim explodes!")
        self.tearDown()

        self.assertTrue(str(oTank2) == 'Jim (DEAD)', 'fire_at target __str__ incorrect when Dead')

Тестовые примеры (как успешные, так и проигранные) работали в Python 3.7. Обратите внимание, что метод sorens захватывает весь вывод stdout между вызовами setup() и teardown(), поэтому я разместил их вокруг определенных действий, которые будут генерировать конкретный вывод, который я хотел проверить. Я предполагаю, что мой подход к миксину - это то, что sorens предназначал для общего повторного использования, но я хотел бы знать, есть ли у кого-нибудь другие рекомендации. Спасибо. [1]: /questions/2260574/kak-utverzhdat-vyivod-s-pomoschyu-testov-na-nos-yunit-test-v-python/55358702#55358702

Основываясь на всех замечательных ответах в этой теме, я решил эту проблему следующим образом. Я хотел, чтобы он был как можно более стоковым. Я расширил механизм модульного тестирования, используяsetUp() захватить sys.stdout а также sys.stderr, добавлены новые API-интерфейсы утверждений, чтобы проверить полученные значения на соответствие ожидаемым, а затем восстановить sys.stdout а также sys.stderr на tearDown(). I did this to keep a similar unit test API as the built-inмодульный тестAPI while still being able to unit test values printed tosys.stdoutorsys.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)
Другие вопросы по тегам