Как утверждать вывод с помощью тестов на нос / юнит-тест в 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 to
sys.stdoutor
sys.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)