วิธียืนยันเอาต์พุตด้วย nosetest / 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!'

แต่ถ้าฉันรัน nosetests ด้วยพารามิเตอร์ -s การทดสอบจะล้มเหลว ฉันจะจับเอาท์พุทด้วยโมดูลที่ไม่กระชับหรือจมูกได้อย่างไร?


คำตอบ:


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บล็อกเราจึงสามารถตั้งค่าบล็อกการจับภาพที่สองในฟังก์ชันเดียวกับอันแรกซึ่งไม่สามารถทำได้โดยใช้ฟังก์ชันการตั้งค่าและการฉีกขาดและจะทำให้เกิดความสับสนเมื่อเขียนในที่สุด บล็อกด้วยตนเอง ความสามารถดังกล่าวมีประโยชน์เมื่อเป้าหมายของการทดสอบคือการเปรียบเทียบผลลัพธ์ของสองฟังก์ชันที่สัมพันธ์กันแทนที่จะเป็นค่าที่คำนวณไว้ล่วงหน้า


นี้ได้ทำงานได้ดีจริงๆสำหรับฉันในpep8radius อย่างไรก็ตามเมื่อเร็ว ๆ นี้ฉันได้ใช้สิ่งนี้อีกครั้งและได้รับข้อผิดพลาดต่อไปนี้เมื่อพิมพ์TypeError: unicode argument expected, got 'str'(ประเภทที่ส่งไปยังการพิมพ์ (str / unicode) ไม่เกี่ยวข้อง)
Andy Hayden

9
อืมมก็อาจเป็นไปได้ว่าในหลาม 2 ที่เราต้องการfrom io import BytesIO as StringIOและในหลาม 3 from io import StringIOเพียง ดูเหมือนจะแก้ไขปัญหาในการทดสอบของฉันฉันคิดว่า
Andy Hayden

4
โอ๊ะขอจบขอโทษสำหรับข้อความมากมาย แค่ชี้แจงสำหรับคนที่เจอสิ่งนี้ python3 ใช้ io.StringIO, python 2 ใช้ StringIO.StringIO! ขอบคุณอีกครั้ง!
Andy Hayden

เหตุใดตัวอย่างทั้งหมดจึงเรียกร้องstrip()ให้unicodeส่งคืนจากStringIO.getvalue()?
Palimondo

1
ไม่ @Vedran. สิ่งนี้อาศัยการผูกชื่อที่เป็นของsysอีกครั้ง ด้วยคำสั่งนำเข้าของคุณคุณกำลังสร้างตัวแปรโลคัลชื่อstderrที่ได้รับสำเนาของค่าในsys.stderr. การเปลี่ยนแปลงอย่างหนึ่งจะไม่สะท้อนให้เห็นในอีกรายการหนึ่ง
Rob Kennedy

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 StringIOใน python 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 ขอบคุณนี่ทำให้การทดสอบบางอย่างของฉันง่ายขึ้น ... สำหรับการดำน้ำที่ฉันเห็นว่าคุณติดดาว .... โลกใบเล็ก!
Jonathon Reinhart

48

ตั้งแต่เวอร์ชัน 2.7 คุณไม่จำเป็นต้องกำหนดใหม่อีกต่อไปsys.stdoutสิ่งนี้มีให้ผ่านbufferแฟล็ก นอกจากนี้ยังเป็นพฤติกรรมเริ่มต้นของ nosetest

นี่คือตัวอย่างที่ล้มเหลวในบริบทที่ไม่ใช่บัฟเฟอร์:

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

33

คำตอบเหล่านี้จำนวนมากล้มเหลวสำหรับฉันเพราะคุณไม่สามารถfrom StringIO import StringIOอยู่ใน Python 3 ได้นี่คือตัวอย่างข้อมูลการทำงานขั้นต่ำตามความคิดเห็นของ @ naxa และ Python Cookbook

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

1
นี่เป็นทางออกเดียวในหน้านี้ที่เหมาะกับฉัน! ขอบคุณ.
Justin Eyster

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

redirect_stdout()และredirect_stderr()ส่งคืนอาร์กิวเมนต์อินพุตของพวกเขา ดังนั้นwith contextlib.redirect_stdout(StringIO()) as temp_stdout:ให้คุณทั้งหมดในบรรทัดเดียว ทดสอบด้วย 3.7.1
Adrian W

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
Edwarric

10

การทดสอบการเขียนมักจะแสดงให้เราเห็นวิธีที่ดีกว่าในการเขียนโค้ดของเรา เช่นเดียวกับคำตอบของ Shane ฉันอยากจะแนะนำวิธีอื่นในการดูสิ่งนี้ คุณต้องการยืนยันว่าโปรแกรมของคุณส่งออกสตริงที่แน่นอนหรือเพียงแค่ว่ามันสร้างสตริงสำหรับเอาต์พุต? การทดสอบนี้ง่ายขึ้นเนื่องจากเราสามารถสันนิษฐานได้ว่าprintคำสั่งPython ทำงานได้อย่างถูกต้อง

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

การทดสอบของคุณง่ายมาก:

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

แน่นอนว่าหากคุณมีความต้องการที่จะทดสอบผลลัพธ์จริงของโปรแกรมคุณก็อย่าลังเลที่จะเพิกเฉย :)


1
แต่ในกรณีนี้ foo จะไม่ถูกทดสอบ ... บางทีนั่นอาจเป็นปัญหา
Pedro Valencia

5
จากมุมมองของคนเจ้าระเบียบในการทดสอบอาจเป็นปัญหา จากมุมมองในทางปฏิบัติถ้าfoo()ไม่ทำอะไรเลยนอกจากเรียก print statement ก็คงไม่ใช่ปัญหา
Alison R.

5

จากคำตอบของ Rob Kennedy ฉันได้เขียนตัวจัดการบริบทเวอร์ชันอิงคลาสเพื่อบัฟเฟอร์เอาต์พุต

การใช้งานเป็นเหมือน:

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และNoumenonแนะนำให้ใช้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)

0

จากคำตอบที่ยอดเยี่ยมทั้งหมดในชุดข้อความนี้ฉันจะแก้ไขได้อย่างไร ฉันต้องการเก็บไว้เป็นสต็อกมากที่สุด ผมเติมกลไกการทดสอบหน่วยที่ใช้setUp()ในการจับภาพsys.stdoutและsys.stderrเพิ่ม API ที่ยืนยันการใหม่ในการตรวจสอบค่าจับกับค่าที่คาดหวังแล้วคืน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)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.