การส่งออกข้อมูลจากการทดสอบหน่วยใน python


115

หากฉันกำลังเขียนการทดสอบหน่วยใน python (โดยใช้โมดูลที่ไม่เหมาะสมที่สุด) เป็นไปได้ไหมที่จะส่งออกข้อมูลจากการทดสอบที่ล้มเหลวดังนั้นฉันจึงสามารถตรวจสอบเพื่อช่วยอนุมานสิ่งที่ทำให้เกิดข้อผิดพลาดได้ ฉันตระหนักถึงความสามารถในการสร้างข้อความที่กำหนดเองซึ่งสามารถนำข้อมูลบางอย่างไปได้ แต่บางครั้งคุณอาจจัดการกับข้อมูลที่ซับซ้อนมากขึ้นซึ่งไม่สามารถแสดงเป็นสตริงได้อย่างง่ายดาย

ตัวอย่างเช่นสมมติว่าคุณมีคลาส Foo และกำลังทดสอบแถบวิธีการโดยใช้ข้อมูลจากรายการที่เรียกว่า testdata:

class TestBar(unittest.TestCase):
    def runTest(self):
        for t1, t2 in testdata:
            f = Foo(t1)
            self.assertEqual(f.bar(t2), 2)

หากการทดสอบล้มเหลวฉันอาจต้องการเอาต์พุต t1, t2 และ / หรือ f เพื่อดูว่าเหตุใดข้อมูลนี้จึงทำให้เกิดความล้มเหลว ฉันหมายความว่าตัวแปรสามารถเข้าถึงได้เหมือนกับตัวแปรอื่น ๆ หลังจากที่รันการทดสอบแล้ว

คำตอบ:


73

คำตอบที่ช้ามากสำหรับคนที่เหมือนฉันมาที่นี่เพื่อหาคำตอบที่ง่ายและรวดเร็ว

ใน Python 2.7 คุณสามารถใช้พารามิเตอร์เพิ่มเติมmsgเพื่อเพิ่มข้อมูลในข้อความแสดงข้อผิดพลาดเช่นนี้:

self.assertEqual(f.bar(t2), 2, msg='{0}, {1}'.format(t1, t2))

เอกสารอย่างเป็นทางการที่นี่


1
ทำงานใน Python 3 ด้วย
MrDBA

18
เอกสารบอกใบ้สิ่งนี้ แต่ควรกล่าวถึงอย่างชัดเจน: โดยค่าเริ่มต้นหากmsgมีการใช้งานจะแทนที่ข้อความแสดงข้อผิดพลาดปกติ ในการmsgต่อท้ายข้อความแสดงข้อผิดพลาดปกติคุณต้องตั้งค่าTestCase.longMessageเป็น True
Catalin Iacob

1
น่ารู้ว่าเราสามารถส่งข้อความแสดงข้อผิดพลาดที่กำหนดเองได้ แต่ฉันสนใจที่จะพิมพ์บางข้อความโดยไม่คำนึงถึงข้อผิดพลาด
Harry Moreno

5
ความคิดเห็นโดย @CatalinIacob ใช้กับ Python 2.x. ในหลาม 3.x, TestCase.longMessageTrueค่าเริ่มต้น
ndmeiri

70

เราใช้โมดูลการบันทึกสำหรับสิ่งนี้

ตัวอย่างเช่น:

import logging
class SomeTest( unittest.TestCase ):
    def testSomething( self ):
        log= logging.getLogger( "SomeTest.testSomething" )
        log.debug( "this= %r", self.this )
        log.debug( "that= %r", self.that )
        # etc.
        self.assertEquals( 3.14, pi )

if __name__ == "__main__":
    logging.basicConfig( stream=sys.stderr )
    logging.getLogger( "SomeTest.testSomething" ).setLevel( logging.DEBUG )
    unittest.main()

ซึ่งช่วยให้เราสามารถเปิดการดีบักสำหรับการทดสอบเฉพาะที่เราทราบว่าล้มเหลวและเราต้องการข้อมูลการดีบักเพิ่มเติม

อย่างไรก็ตามวิธีที่ฉันชอบคือไม่ต้องเสียเวลาไปกับการดีบักมากนัก แต่ใช้เวลาเขียนแบบทดสอบที่ละเอียดมากขึ้นเพื่อเปิดเผยปัญหา


จะเกิดอะไรขึ้นถ้าฉันเรียก method foo ภายใน testSomething และบันทึกบางสิ่ง ฉันจะดูผลลัพธ์ได้อย่างไรโดยไม่ต้องส่งคนตัดไม้ไปที่ foo
simao

@simao: คืออะไรfoo? ฟังก์ชั่นแยกต่างหาก? ฟังก์ชันวิธีการของSomeTest? ในกรณีแรกฟังก์ชันสามารถมีคนตัดไม้ของตัวเองได้ ในกรณีที่สองฟังก์ชันวิธีอื่นสามารถมีคนตัดไม้เป็นของตัวเอง คุณทราบวิธีการloggingทำงานของแพ็คเกจหรือไม่? คนตัดไม้หลายคนถือเป็นบรรทัดฐาน
ล็อต

8
ฉันตั้งค่าการบันทึกตามวิธีที่คุณระบุ ฉันคิดว่ามันใช้งานได้ แต่ฉันจะเห็นผลลัพธ์ได้ที่ไหน มันไม่ได้ส่งออกไปยังคอนโซล ฉันพยายามกำหนดค่าด้วยการบันทึกไฟล์ แต่ก็ไม่ได้ผลลัพธ์ใด ๆ
MikeyE

"อย่างไรก็ตามวิธีที่ฉันชอบคือไม่ต้องเสียเวลาไปกับการแก้ไขข้อบกพร่องมากนัก แต่ใช้เวลาเขียนแบบทดสอบที่ละเอียดมากขึ้นเพื่อเปิดเผยปัญหา" -- พูดได้ดี!
Seth

34

คุณสามารถใช้คำสั่งพิมพ์ธรรมดาหรือวิธีอื่น ๆ ในการเขียนถึง stdout คุณยังสามารถเรียกใช้โปรแกรมแก้ไขข้อบกพร่อง Python ได้จากทุกที่ในการทดสอบของคุณ

หากคุณใช้จมูกเพื่อเรียกใช้การทดสอบของคุณ (ซึ่งฉันแนะนำ) มันจะรวบรวม stdout สำหรับการทดสอบแต่ละครั้งและแสดงให้คุณเห็นเฉพาะในกรณีที่การทดสอบล้มเหลวดังนั้นคุณไม่จำเป็นต้องอยู่กับผลลัพธ์ที่รกรุงรังเมื่อการทดสอบผ่านไป

จมูกยังมีสวิตช์เพื่อแสดงตัวแปรที่กล่าวถึงในการยืนยันโดยอัตโนมัติหรือเพื่อเรียกใช้ดีบักเกอร์ในการทดสอบที่ล้มเหลว ตัวอย่างเช่น-s( --nocapture) ป้องกันการจับ stdout


น่าเสียดายที่จมูกดูเหมือนจะไม่รวบรวมบันทึกที่เขียนถึง stdout / err โดยใช้กรอบการบันทึก ฉันมีprintและlog.debug()อยู่ติดกันและเปิดใช้งานDEBUGการบันทึกที่รูทจากsetUp()เมธอดอย่างชัดเจนแต่มีเพียงprintผลลัพธ์เท่านั้นที่ปรากฏขึ้น
haridsv

7
nosetests -sแสดงเนื้อหาของ stdout ว่ามีข้อผิดพลาดหรือไม่ - สิ่งที่ฉันคิดว่ามีประโยชน์
hargriffle

ฉันไม่พบสวิตช์ที่จะแสดงตัวแปรโดยอัตโนมัติในเอกสารจมูก คุณช่วยชี้ให้ฉันดูบางสิ่งที่อธิบายพวกเขาได้ไหม
ABM

ฉันไม่รู้วิธีแสดงตัวแปรโดยอัตโนมัติจากจมูกหรือไม่เหมาะสมที่สุด ฉันพิมพ์สิ่งที่ฉันต้องการเห็นในการทดสอบของฉัน
Ned Batchelder

16

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

คุณสามารถใช้อ็อบเจ็กต์ TestResult ที่ส่งคืนโดยTestRunner.run ()สำหรับการวิเคราะห์และประมวลผลผลลัพธ์ โดยเฉพาะ TestResult.errors และ TestResult.failures

เกี่ยวกับวัตถุ TestResults:

http://docs.python.org/library/unittest.html#id3

และรหัสบางส่วนเพื่อชี้ให้คุณไปในทิศทางที่ถูกต้อง:

>>> import random
>>> import unittest
>>>
>>> class TestSequenceFunctions(unittest.TestCase):
...     def setUp(self):
...         self.seq = range(5)
...     def testshuffle(self):
...         # make sure the shuffled sequence does not lose any elements
...         random.shuffle(self.seq)
...         self.seq.sort()
...         self.assertEqual(self.seq, range(10))
...     def testchoice(self):
...         element = random.choice(self.seq)
...         error_test = 1/0
...         self.assert_(element in self.seq)
...     def testsample(self):
...         self.assertRaises(ValueError, random.sample, self.seq, 20)
...         for element in random.sample(self.seq, 5):
...             self.assert_(element in self.seq)
...
>>> suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions)
>>> testResult = unittest.TextTestRunner(verbosity=2).run(suite)
testchoice (__main__.TestSequenceFunctions) ... ERROR
testsample (__main__.TestSequenceFunctions) ... ok
testshuffle (__main__.TestSequenceFunctions) ... FAIL

======================================================================
ERROR: testchoice (__main__.TestSequenceFunctions)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 11, in testchoice
ZeroDivisionError: integer division or modulo by zero

======================================================================
FAIL: testshuffle (__main__.TestSequenceFunctions)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 8, in testshuffle
AssertionError: [0, 1, 2, 3, 4] != [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

----------------------------------------------------------------------
Ran 3 tests in 0.031s

FAILED (failures=1, errors=1)
>>>
>>> testResult.errors
[(<__main__.TestSequenceFunctions testMethod=testchoice>, 'Traceback (most recent call last):\n  File "<stdin>"
, line 11, in testchoice\nZeroDivisionError: integer division or modulo by zero\n')]
>>>
>>> testResult.failures
[(<__main__.TestSequenceFunctions testMethod=testshuffle>, 'Traceback (most recent call last):\n  File "<stdin>
", line 8, in testshuffle\nAssertionError: [0, 1, 2, 3, 4] != [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n')]
>>>

5

อีกทางเลือกหนึ่ง - เริ่มการดีบักเกอร์ที่การทดสอบล้มเหลว

ลองเรียกใช้การทดสอบของคุณด้วย Testoob (มันจะเรียกใช้ชุดเครื่องมือที่ไม่เหมาะสมที่สุดของคุณโดยไม่มีการเปลี่ยนแปลง) และคุณสามารถใช้สวิตช์บรรทัดคำสั่ง '--debug' เพื่อเปิดดีบักเกอร์เมื่อการทดสอบล้มเหลว

นี่คือเซสชันเทอร์มินัลบน windows:

C:\work> testoob tests.py --debug
F
Debugging for failure in test: test_foo (tests.MyTests.test_foo)
> c:\python25\lib\unittest.py(334)failUnlessEqual()
-> (msg or '%r != %r' % (first, second))
(Pdb) up
> c:\work\tests.py(6)test_foo()
-> self.assertEqual(x, y)
(Pdb) l
  1     from unittest import TestCase
  2     class MyTests(TestCase):
  3       def test_foo(self):
  4         x = 1
  5         y = 2
  6  ->     self.assertEqual(x, y)
[EOF]
(Pdb)

2
Nose ( nose.readthedocs.org/en/latest/index.html ) เป็นอีกหนึ่งเฟรมเวิร์กที่มีตัวเลือก "เริ่มเซสชันการดีบักเกอร์" ฉันเรียกใช้ด้วย '-sx --pdb --pdb-failures' ซึ่งไม่กินเอาต์พุตหยุดหลังจากความล้มเหลวครั้งแรกและลดลงใน pdb ในข้อยกเว้นและการทดสอบล้มเหลว สิ่งนี้ได้ลบความต้องการของฉันสำหรับข้อความแสดงข้อผิดพลาดมากมายเว้นแต่ฉันจะขี้เกียจและทดสอบวนซ้ำ
jwhitlock

5

วิธีที่ฉันใช้นั้นง่ายมาก ฉันแค่บันทึกเป็นคำเตือนเพื่อให้มันปรากฏขึ้นจริง

import logging

class TestBar(unittest.TestCase):
    def runTest(self):

       #this line is important
       logging.basicConfig()
       log = logging.getLogger("LOG")

       for t1, t2 in testdata:
         f = Foo(t1)
         self.assertEqual(f.bar(t2), 2)
         log.warning(t1)

จะได้ผลหรือไม่ถ้าการทดสอบสำเร็จ? ในกรณีของฉันคำเตือนจะปรากฏขึ้นก็ต่อเมื่อการทดสอบล้มเหลว
Shreya Maria

@ShreyaMaria ใช่แล้ว
Orane

5

ฉันคิดว่าฉันอาจจะคิดมากเรื่องนี้ วิธีหนึ่งที่ฉันคิดขึ้นมาได้ก็คือการมีตัวแปรส่วนกลางที่รวบรวมข้อมูลการวินิจฉัย

อะไรทำนองนี้:

log1 = dict()
class TestBar(unittest.TestCase):
    def runTest(self):
        for t1, t2 in testdata:
            f = Foo(t1) 
            if f.bar(t2) != 2: 
                log1("TestBar.runTest") = (f, t1, t2)
                self.fail("f.bar(t2) != 2")

ขอบคุณสำหรับคำตอบ พวกเขาให้แนวคิดทางเลือกในการบันทึกข้อมูลจากการทดสอบหน่วยใน python


2

ใช้การบันทึก:

import unittest
import logging
import inspect
import os

logging_level = logging.INFO

try:
    log_file = os.environ["LOG_FILE"]
except KeyError:
    log_file = None

def logger(stack=None):
    if not hasattr(logger, "initialized"):
        logging.basicConfig(filename=log_file, level=logging_level)
        logger.initialized = True
    if not stack:
        stack = inspect.stack()
    name = stack[1][3]
    try:
        name = stack[1][0].f_locals["self"].__class__.__name__ + "." + name
    except KeyError:
        pass
    return logging.getLogger(name)

def todo(msg):
    logger(inspect.stack()).warning("TODO: {}".format(msg))

def get_pi():
    logger().info("sorry, I know only three digits")
    return 3.14

class Test(unittest.TestCase):

    def testName(self):
        todo("use a better get_pi")
        pi = get_pi()
        logger().info("pi = {}".format(pi))
        todo("check more digits in pi")
        self.assertAlmostEqual(pi, 3.14)
        logger().debug("end of this test")
        pass

การใช้งาน:

# LOG_FILE=/tmp/log python3 -m unittest LoggerDemo
.
----------------------------------------------------------------------
Ran 1 test in 0.047s

OK
# cat /tmp/log
WARNING:Test.testName:TODO: use a better get_pi
INFO:get_pi:sorry, I know only three digits
INFO:Test.testName:pi = 3.14
WARNING:Test.testName:TODO: check more digits in pi

หากคุณไม่ได้ตั้งค่าการเข้าสู่ระบบจะมีการLOG_FILEstderr


2

คุณสามารถใช้loggingโมดูลสำหรับสิ่งนั้นได้

ดังนั้นในรหัสทดสอบหน่วยให้ใช้:

import logging as log

def test_foo(self):
    log.debug("Some debug message.")
    log.info("Some info message.")
    log.warning("Some warning message.")
    log.error("Some error message.")

ตามค่าเริ่มต้นคำเตือนและข้อผิดพลาดจะถูกส่งออกไป/dev/stderrดังนั้นจึงควรมองเห็นได้บนคอนโซล

ในการปรับแต่งบันทึก (เช่นการจัดรูปแบบ) ให้ลองทำตามตัวอย่างต่อไปนี้:

# Set-up logger
if args.verbose or args.debug:
    logging.basicConfig( stream=sys.stdout )
    root = logging.getLogger()
    root.setLevel(logging.INFO if args.verbose else logging.DEBUG)
    ch = logging.StreamHandler(sys.stdout)
    ch.setLevel(logging.INFO if args.verbose else logging.DEBUG)
    ch.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(name)s: %(message)s'))
    root.addHandler(ch)
else:
    logging.basicConfig(stream=sys.stderr)

2

สิ่งที่ฉันทำในกรณีเหล่านี้คือการมีlog.debug()ข้อความบางอย่างในใบสมัครของฉัน เนื่องจากระดับการบันทึกเริ่มต้นคือWARNINGข้อความดังกล่าวจึงไม่แสดงในการดำเนินการตามปกติ

จากนั้นในการรวมกันฉันจะเปลี่ยนระดับการบันทึกเป็นDEBUGเพื่อให้ข้อความดังกล่าวแสดงขึ้นขณะเรียกใช้

import logging

log.debug("Some messages to be shown just when debugging or unittesting")

ใน unittests:

# Set log level
loglevel = logging.DEBUG
logging.basicConfig(level=loglevel)



ดูตัวอย่างเต็ม:

นี่คือdaikiri.pyคลาสพื้นฐานที่ใช้ Daikiri พร้อมชื่อและราคา มีวิธีการmake_discount()คืนราคาของ daikiri เฉพาะนั้นหลังจากใช้ส่วนลดที่กำหนด:

import logging

log = logging.getLogger(__name__)

class Daikiri(object):
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def make_discount(self, percentage):
        log.debug("Deducting discount...")  # I want to see this message
        return self.price * percentage

จากนั้นฉันสร้าง unittest test_daikiri.pyที่ตรวจสอบการใช้งาน:

import unittest
import logging
from .daikiri import Daikiri


class TestDaikiri(unittest.TestCase):
    def setUp(self):
        # Changing log level to DEBUG
        loglevel = logging.DEBUG
        logging.basicConfig(level=loglevel)

        self.mydaikiri = Daikiri("cuban", 25)

    def test_drop_price(self):
        new_price = self.mydaikiri.make_discount(0)
        self.assertEqual(new_price, 0)

if __name__ == "__main__":
    unittest.main()

ดังนั้นเมื่อฉันดำเนินการฉันจะได้รับlog.debugข้อความ:

$ python -m test_daikiri
DEBUG:daikiri:Deducting discount...
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

1

ตรวจสอบการติดตามจะช่วยให้คุณได้รับตัวแปรภายในเครื่องหลังจากที่มีการโยนข้อยกเว้น จากนั้นคุณสามารถรวมการทดสอบหน่วยด้วยมัณฑนากรเช่นการทดสอบต่อไปนี้เพื่อบันทึกตัวแปรท้องถิ่นเหล่านั้นไว้สำหรับการตรวจสอบในระหว่างการชันสูตร

import random
import unittest
import inspect


def store_result(f):
    """
    Store the results of a test
    On success, store the return value.
    On failure, store the local variables where the exception was thrown.
    """
    def wrapped(self):
        if 'results' not in self.__dict__:
            self.results = {}
        # If a test throws an exception, store local variables in results:
        try:
            result = f(self)
        except Exception as e:
            self.results[f.__name__] = {'success':False, 'locals':inspect.trace()[-1][0].f_locals}
            raise e
        self.results[f.__name__] = {'success':True, 'result':result}
        return result
    return wrapped

def suite_results(suite):
    """
    Get all the results from a test suite
    """
    ans = {}
    for test in suite:
        if 'results' in test.__dict__:
            ans.update(test.results)
    return ans

# Example:
class TestSequenceFunctions(unittest.TestCase):

    def setUp(self):
        self.seq = range(10)

    @store_result
    def test_shuffle(self):
        # make sure the shuffled sequence does not lose any elements
        random.shuffle(self.seq)
        self.seq.sort()
        self.assertEqual(self.seq, range(10))
        # should raise an exception for an immutable sequence
        self.assertRaises(TypeError, random.shuffle, (1,2,3))
        return {1:2}

    @store_result
    def test_choice(self):
        element = random.choice(self.seq)
        self.assertTrue(element in self.seq)
        return {7:2}

    @store_result
    def test_sample(self):
        x = 799
        with self.assertRaises(ValueError):
            random.sample(self.seq, 20)
        for element in random.sample(self.seq, 5):
            self.assertTrue(element in self.seq)
        return {1:99999}


suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions)
unittest.TextTestRunner(verbosity=2).run(suite)

from pprint import pprint
pprint(suite_results(suite))

บรรทัดสุดท้ายจะพิมพ์ค่าที่ส่งคืนเมื่อการทดสอบสำเร็จและตัวแปรโลคัลในกรณีนี้ x เมื่อล้มเหลว:

{'test_choice': {'result': {7: 2}, 'success': True},
 'test_sample': {'locals': {'self': <__main__.TestSequenceFunctions testMethod=test_sample>,
                            'x': 799},
                 'success': False},
 'test_shuffle': {'result': {1: 2}, 'success': True}}

Har det gøy :-)


0

แล้วการจับข้อยกเว้นที่สร้างขึ้นจากความล้มเหลวในการยืนยันล่ะ ในบล็อกการจับของคุณคุณสามารถส่งออกข้อมูลตามที่คุณต้องการได้ทุกที่ จากนั้นเมื่อคุณทำเสร็จแล้วคุณสามารถโยนข้อยกเว้นใหม่ได้ ผู้ทดสอบคงไม่ทราบความแตกต่าง

ข้อจำกัดความรับผิดชอบ: ฉันไม่ได้ลองใช้กรอบการทดสอบหน่วยของ python แต่มีกับกรอบการทดสอบหน่วยอื่น ๆ



-1

การขยายคำตอบของ @FC สิ่งนี้ใช้ได้ดีสำหรับฉัน:

class MyTest(unittest.TestCase):
    def messenger(self, message):
        try:
            self.assertEqual(1, 2, msg=message)
        except AssertionError as e:      
            print "\nMESSENGER OUTPUT: %s" % str(e),
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.