Làm thế nào để xác nhận đầu ra với nosetest / unittest trong python?


114

Tôi đang viết các bài kiểm tra cho một hàm như hàm tiếp theo:

def foo():
    print 'hello world!'

Vì vậy, khi tôi muốn kiểm tra chức năng này, mã sẽ như thế này:

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

Nhưng nếu tôi chạy thử nghiệm với tham số -s thì thử nghiệm bị treo. Làm thế nào tôi có thể bắt đầu ra với mô-đun mũi hoặc đơn nhất?


Câu trả lời:


124

Tôi sử dụng trình quản lý ngữ cảnh này để nắm bắt đầu ra. Cuối cùng, nó sử dụng kỹ thuật tương tự như một số câu trả lời khác bằng cách tạm thời thay thế sys.stdout. Tôi thích trình quản lý ngữ cảnh hơn vì nó gói gọn tất cả việc ghi sổ vào một chức năng duy nhất, vì vậy tôi không phải viết lại bất kỳ mã try-final nào và tôi không phải viết các hàm thiết lập và xé nhỏ chỉ cho việc này.

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

Sử dụng nó như thế này:

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

Hơn nữa, vì trạng thái đầu ra ban đầu được khôi phục khi thoát khỏi withkhối, chúng ta có thể thiết lập khối chụp thứ hai trong cùng chức năng với khối đầu tiên, điều này không thể thực hiện được bằng cách sử dụng các hàm setup và teardown, và sẽ bị dài dòng khi viết try-last khối thủ công. Khả năng đó rất hữu ích khi mục tiêu của một bài kiểm tra là so sánh kết quả của hai hàm tương đối với nhau thay vì với một số giá trị được tính toán trước.


Điều này đã hoạt động thực sự hiệu quả đối với tôi trong pep8radius . Tuy nhiên, gần đây, tôi đã sử dụng lại điều này và gặp lỗi sau khi in TypeError: unicode argument expected, got 'str'(kiểu được chuyển để in (str / unicode) không liên quan).
Andy Hayden

9
Hmmm có thể là trong python 2 chúng ta muốn from io import BytesIO as StringIOvà trong python 3 chỉ from io import StringIO. Tôi nghĩ có vẻ như để khắc phục sự cố trong các thử nghiệm của mình.
Andy Hayden

4
Ooop, chỉ để kết thúc, xin lỗi vì rất nhiều tin nhắn. Chỉ để làm rõ cho những người tìm thấy điều này: python3 sử dụng io.StringIO, python 2 sử dụng StringIO.StringIO! Cảm ơn một lần nữa!
Andy Hayden,

Tại sao tất cả các ví dụ ở đây gọi strip()unicodetrả về từ StringIO.getvalue()?
Palimondo

1
Không, @Vedran. Điều này phụ thuộc vào việc gắn lại tên thuộc về sys. Với tuyên bố nhập của bạn, bạn đang tạo ra một địa phương biến có tên stderrđã nhận được một bản sao của các giá trị trong sys.stderr. Thay đổi đối với một không được phản ánh trong khác.
Rob Kennedy

60

Nếu bạn thực sự muốn làm điều này, bạn có thể gán lại sys.stdout trong suốt thời gian kiểm tra.

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

Tuy nhiên, nếu tôi đang viết mã này, tôi muốn chuyển một outtham số tùy chọn cho foohàm.

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

Sau đó, kiểm tra đơn giản hơn nhiều:

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
Lưu ý: Trong python 3.x, StringIOlớp bây giờ phải được nhập từ iomô-đun. from io import StringIOhoạt động trong python 2.6+.
Bryan P

2
Nếu bạn sử dụng from io import StringIOtrong python 2, bạn sẽ nhận được TypeError: unicode argument expected, got 'str'khi in.
matiasg

9
Lưu ý nhanh chóng: Trong python 3.4, bạn có thể sử dụng contextlib.redirect_stdout quản lý bối cảnh để làm điều này trong một cách mà là ngoại lệ an toàn:with redirect_stdout(out):
Lucretiel

2
Bạn không cần phải làm như vậy saved_stdout = sys.stdout, bạn luôn có một giới thiệu ma thuật cho điều này sys.__stdout__, ví dụ, bạn chỉ cần sys.stdout = sys.__stdout__trong quá trình dọn dẹp của mình.
ThorSummoner,

@ThorSummoner Cảm ơn, điều này vừa đơn giản hóa một số thử nghiệm của tôi ... cho môn lặn mà tôi thấy bạn đã gắn dấu sao .... thế giới nhỏ!
Jonathon Reinhart

48

Kể từ phiên bản 2.7, bạn không cần phải gán lại nữa sys.stdout, điều này được cung cấp thông qua buffercờ . Hơn nữa, nó là hành vi mặc định của nosetest.

Đây là một mẫu không thành công trong ngữ cảnh không có bộ đệm:

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

Bạn có thể đặt đệm thông qua unit2dòng lệnh cờ -b, --bufferhoặc unittest.maintùy chọn. Điều ngược lại đạt được thông qua nosetestcờ --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)

Lưu ý rằng điều này tương tác với --nocapture; đặc biệt, nếu cờ này được đặt, chế độ đệm sẽ bị tắt. Vì vậy, bạn có tùy chọn có thể xem đầu ra trên thiết bị đầu cuối hoặc có thể kiểm tra xem đầu ra có như mong đợi hay không.
ijoseph

1
Có thể bật và tắt tính năng này cho mỗi lần kiểm tra không, vì điều này làm cho việc gỡ lỗi rất khó khăn khi sử dụng một cái gì đó như ipdb.set_trace ()?
Lqueryvg

33

Rất nhiều câu trả lời không thành công đối với tôi vì bạn không thể sử dụng from StringIO import StringIOPython 3. Đây là một đoạn mã hoạt động tối thiểu dựa trên nhận xét của @ naxa và Sách dạy nấu ăn 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
Tôi thích cái này cho Python 3, nó sạch sẽ!
Sylhare

1
Đây là giải pháp duy nhất trên trang này phù hợp với tôi! Cảm ơn bạn.
Justin Eyster

24

Trong python 3.5, bạn có thể sử dụng contextlib.redirect_stdout()StringIO(). Đây là sửa đổi mã của bạn

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

Câu trả lời chính xác! Theo tài liệu này đã được thêm vào Python 3.4.
Hypercube

Đó là 3,4 cho redirect_stdout và 3,5 cho redirect_stderr. có lẽ đó là nơi nảy sinh sự nhầm lẫn!
rbennell

redirect_stdout()redirect_stderr()trả về đối số đầu vào của chúng. Vì vậy, with contextlib.redirect_stdout(StringIO()) as temp_stdout:cung cấp cho bạn tất cả trong một dòng. Đã thử nghiệm với 3.7.1.
Adrian W

17

Tôi chỉ mới học Python và thấy mình đang vật lộn với một vấn đề tương tự như vấn đề ở trên với các bài kiểm tra đơn vị cho các phương pháp có đầu ra. Bài kiểm tra đơn vị vượt qua của tôi cho mô-đun foo ở trên đã kết thúc như thế này:

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
Bạn có thể muốn làm một sys.stdout.getvalue().strip()và không gian lận so với \n:)
Silviu

Mô-đun StringIO không được dùng nữa. Thay vào đófrom io import StringIO
Edwarric

10

Viết bài kiểm tra thường cho chúng ta thấy một cách tốt hơn để viết mã của chúng ta. Tương tự như câu trả lời của Shane, tôi muốn đề xuất một cách khác để xem xét vấn đề này. Bạn có thực sự muốn khẳng định rằng chương trình của bạn đã xuất ra một chuỗi nhất định hay chỉ là nó đã xây dựng một chuỗi nhất định cho đầu ra? Điều này trở nên dễ kiểm tra hơn, vì chúng ta có thể giả định rằng printcâu lệnh Python thực hiện đúng công việc của nó.

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

Sau đó, thử nghiệm của bạn rất đơn giản:

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

Tất nhiên, nếu bạn thực sự có nhu cầu kiểm tra đầu ra thực tế của chương trình của mình thì bạn cứ thoải mái bỏ qua. :)


1
nhưng trong trường hợp này foo sẽ không được kiểm tra ... có lẽ đó là một vấn đề
Pedro Valencia

5
Từ quan điểm của một người theo chủ nghĩa thuần túy thử nghiệm, có lẽ đó là một vấn đề. Từ quan điểm thực tế, nếu foo()không làm bất cứ điều gì ngoài việc gọi câu lệnh in, nó có lẽ không phải là vấn đề.
Alison R.

5

Dựa trên câu trả lời của Rob Kennedy, tôi đã viết một phiên bản dựa trên lớp của trình quản lý ngữ cảnh để đệm đầu ra.

Cách sử dụng như sau:

with OutputBuffer() as bf:
    print('hello world')
assert bf.out == 'hello world\n'

Đây là cách triển khai:

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

Hoặc xem xét sử dụng pytest, nó có hỗ trợ tích hợp để xác nhận stdout và stderr. Xem tài liệu

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"

Tốt. Bạn có thể đưa vào một ví dụ tối thiểu vì các liên kết có thể biến mất và nội dung có thể thay đổi không?
KobeJohn

2

Cả n611x007Noumenon đều đã được đề xuất sử dụng unittest.mock, nhưng câu trả lời này điều chỉnh Acumenus để chỉ ra cách bạn có thể dễ dàng kết hợp unittest.TestCasecác phương pháp để tương tác với một kẻ bị chế nhạo 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

Dựa trên tất cả các câu trả lời tuyệt vời trong chủ đề này, đây là cách tôi giải quyết nó. Tôi muốn giữ nó càng nhiều càng tốt. Tôi tăng cường cơ chế kiểm tra đơn vị sử dụng setUp()để chụp sys.stdoutsys.stderr, thêm API khẳng định mới để kiểm tra các giá trị bắt chống lại một giá trị kỳ vọng và sau đó khôi phục sys.stdoutsys.stderrkhi 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()

Khi chạy thử nghiệm đơn vị, đầu ra là:

$ 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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.