Python: написать юнит-тест для консольной печати

Функция foo печатает на консоль. Я хочу проверить консоль печати. Как я могу добиться этого в Python?

Необходимо протестировать эту функцию, не имеет оператора возврата:

def foo(inStr):
   print "hi"+inStr

Мой тест:

def test_foo():
    cmdProcess = subprocess.Popen(foo("test"), stdout=subprocess.PIPE)
    cmdOut = cmdProcess.communicate()[0]
    self.assertEquals("hitest", cmdOut)

7 ответов

Решение

Вы можете легко захватить стандартный вывод, просто временно перенаправив sys.stdout к StringIO объект, следующим образом:

import StringIO
import sys

def foo(inStr):
    print "hi"+inStr

def test_foo():
    capturedOutput = StringIO.StringIO()          # Create StringIO object
    sys.stdout = capturedOutput                   #  and redirect stdout.
    foo('test')                                   # Call unchanged function.
    sys.stdout = sys.__stdout__                   # Reset redirect.
    print 'Captured', capturedOutput.getvalue()   # Now works as before.

test_foo()

Выход этой программы:

Captured hitest

показывая, что перенаправление успешно захватило выходные данные и что вы смогли восстановить поток вывода до того, что было до того, как вы начали захват.


Обратите внимание, что приведенный выше код для Python 2.7, как показывает вопрос. Python 3 немного отличается:

import io
import sys

def foo(inStr):
    print ("hi"+inStr)

def test_foo():
    capturedOutput = io.StringIO()                  # Create StringIO object
    sys.stdout = capturedOutput                     #  and redirect stdout.
    foo('test')                                     # Call function.
    sys.stdout = sys.__stdout__                     # Reset redirect.
    print ('Captured', capturedOutput.getvalue())   # Now works as before.

test_foo()

Этот пример Python 3 основан на примере paxdiablo. Оно использует unittest.mock, Он использует многократно используемый вспомогательный метод для создания утверждения.

import io
import unittest
import unittest.mock

from .solution import fizzbuzz


class TestFizzBuzz(unittest.TestCase):

    @unittest.mock.patch('sys.stdout', new_callable=io.StringIO)
    def assert_stdout(self, n, expected_output, mock_stdout):
        fizzbuzz(n)
        self.assertEqual(mock_stdout.getvalue(), expected_output)

    def test_only_numbers(self):
        self.assert_stdout(2, '1\n2\n')

Универсальный TestStdout класс, возможно mixin, в принципе может быть получен из вышеупомянутого.

Если вам случится использовать pytest, имеет встроенную запись захвата. Пример (pytest тесты):

def eggs():
    print('eggs')


def test_spam(capsys):
    eggs()
    captured = capsys.readouterr()
    assert captured.out == 'eggs\n'

Вы также можете использовать его с unittest тестовые классы, хотя вам необходимо вставить объект фикстуры в тестовый класс, например, с помощью autouse fixture:

import unittest
import pytest


class TestSpam(unittest.TestCase):

    @pytest.fixture(autouse=True)
    def _pass_fixtures(self, capsys):
        self.capsys = capsys

    def test_eggs(self):
        eggs()
        captured = self.capsys.readouterr()
        self.assertEqual('eggs\n', captured.out)

Проверьте Доступ к захваченному выводу из тестовой функции для получения дополнительной информации.

Вы также можете использовать макет пакета, как показано ниже, который является примером из https://realpython.com/lessons/mocking-print-unit-tests.

from mock import patch

def greet(name):
    print('Hello ', name)

@patch('builtins.print')
def test_greet(mock_print):
    # The actual test
    greet('John')
    mock_print.assert_called_with('Hello ', 'John')
    greet('Eric')
    mock_print.assert_called_with('Hello ', 'Eric')

Ответ на @Acumenus говорит:

Он также использует многоразовый вспомогательный метод assert_stdout, хотя этот вспомогательный метод специфичен для тестируемой функции.

жирная часть кажется большим недостатком, поэтому я бы сделал следующее:

      class TestCase(unittest.TestCase):

    def assertStdout(self, expected_output):
        return _AssertStdoutContext(self, expected_output)



class _AssertStdoutContext:

    def __init__(self, testcase, expected):
        self.testcase = testcase
        self.expected = expected
        self.captured = io.StringIO()

    def __enter__(self):
        sys.stdout = self.captured
        return self

    def __exit__(self, exc_type, exc_value, tb):
        sys.stdout = sys.__stdout__
        captured = self.captured.getvalue()
        self.testcase.assertEqual(captured, self.expected)

это позволяет гораздо лучше и удобнее использовать повторно:

      def test_print(self):
    with self.assertStdout("test\n"):
        print("test")

с помощью простого диспетчера контекста. (Также может быть желательно добавить "\n" к expected_output поскольку print() по умолчанию добавляет новую строку.)

Вы также можете получить стандартный вывод метода, используяcontextlib.redirect_stdout:

      import unittest
from contextlib import redirect_stdout
from io import StringIO

class TestMyStuff(unittest.TestCase):
    # ...
    def test_stdout(self):
        with redirect_stdout(StringIO()) as sout:
            my_command_that_prints_to_stdout()
        
        # the stream replacing `stdout` is available outside the `with`
        # you may wish to strip the trailing newline
        retval = sout.getvalue().rstrip('\n')

        # test the string captured from `stdout`
        self.assertEqual(retval, "whatever_retval_should_be")

Дает вам локальное решение. Также можно зафиксировать стандартную ошибку, используяcontextlib.redirect_stderr().

Другой вариант опирается наloggingмодуль, а неprint(). Этот модуль также предлагает, когда использоватьprint в документации :

Отображение вывода консоли для обычного использования сценария или программы командной строки

PyTest имеет встроенную поддержку тестирования сообщений журнала.

Другие вопросы по тегам