Python unittest ตรงข้ามกับ assertRaises


374

ฉันต้องการเขียนการทดสอบเพื่อพิสูจน์ว่าข้อยกเว้นไม่ได้เกิดขึ้นในสถานการณ์ที่กำหนด

มันตรงไปตรงมาทดสอบว่ายกเว้นถูกยก ...

sInvalidPath=AlwaysSuppliesAnInvalidPath()
self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath) 

... แต่คุณจะทำสิ่งตรงกันข้ามได้อย่างไร

อย่างนี้ฉันคือสิ่งที่ฉันหลังจาก ...

sValidPath=AlwaysSuppliesAValidPath()
self.assertNotRaises(PathIsNotAValidOne, MyObject, sValidPath) 

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


ปรากฎว่าในความเป็นจริงคุณสามารถใช้assertNotRaisesวิธีการที่ใช้ 90% ของรหัส / พฤติกรรมด้วยassertRaisesในบรรทัดรหัสประมาณ ~ 30-ish ดูคำตอบของฉันด้านล่างสำหรับรายละเอียด
โทร

ฉันต้องการสิ่งนี้เพื่อให้ฉันสามารถเปรียบเทียบทั้งสองฟังก์ชั่นด้วยhypothesisเพื่อให้แน่ใจว่าพวกเขาสร้างเอาต์พุตเดียวกันสำหรับอินพุตทุกชนิดในขณะเดียวกันก็ไม่ต้องสนใจเคสที่ซึ่งต้นฉบับยกข้อยกเว้นขึ้นมา assume(func(a))ไม่ทำงานเนื่องจากเอาต์พุตสามารถเป็นอาร์เรย์ที่มีค่าความจริงที่กำกวม ดังนั้นฉันแค่ต้องการเรียกฟังก์ชั่นและรับTrueถ้ามันไม่ล้มเหลว assume(func(a) is not None)ทำงานฉันเดา
endolith

คำตอบ:


394
def run_test(self):
    try:
        myFunc()
    except ExceptionType:
        self.fail("myFunc() raised ExceptionType unexpectedly!")

32
@hiwaylon - ไม่นี่เป็นทางออกที่ถูกต้องในความเป็นจริง โซลูชันที่เสนอโดยผู้ใช้ 9876 มีข้อบกพร่องด้านแนวคิด: หากคุณทดสอบการพูดที่ไม่ได้ยกValueErrorมา แต่ValueErrorยกขึ้นแทนการทดสอบของคุณจะต้องจบลงด้วยเงื่อนไขความล้มเหลวไม่ใช่ข้อผิดพลาดอย่างใดอย่างหนึ่ง ในทางกลับกันหากในการใช้งานรหัสเดียวกันคุณจะต้องเพิ่มขึ้นKeyErrorซึ่งจะเป็นข้อผิดพลาดไม่ใช่ความล้มเหลว ในภาษาไพ ธ อน - แตกต่างจากภาษาอื่น - มีการใช้ข้อยกเว้นเป็นประจำสำหรับโฟลว์การควบคุมนี่คือสาเหตุที่เรามีexcept <ExceptionName>ไวยากรณ์จริง ๆ ในเรื่องนั้นการแก้ปัญหาของ user9876 นั้นผิด
แม็ค

@mac - นี่เป็นวิธีที่ถูกต้องหรือไม่ stackoverflow.com/a/4711722/6648326
MasterJoe2

อันนี้มีผลโชคร้ายของการแสดงความคุ้มครอง <100% (ยกเว้นจะไม่เกิดขึ้น) สำหรับการทดสอบ
Shay

3
@Shay, IMO คุณควรแยกไฟล์ทดสอบออกจากรายงานการรายงานตัวเสมอ (เนื่องจากไฟล์เหล่านั้นทำงานเกือบ 100% ตามคำจำกัดความคุณจะเพิ่มความผิดพลาดให้กับรายงาน)
ซอสบาร์บีคิวสูตรดั้งเดิม

@ original-bbq-sauce ไม่ว่าจะปล่อยให้ฉันเปิดการทดสอบข้ามโดยไม่ได้ตั้งใจ เช่นพิมพ์ผิดในชื่อทดสอบ (ttst_function) การกำหนดค่าการรันผิดใน pycharm ฯลฯ ?
Shay

67

สวัสดี - ฉันต้องการเขียนแบบทดสอบเพื่อพิสูจน์ว่าข้อยกเว้นไม่ได้เกิดขึ้นในสถานการณ์ที่กำหนด

นั่นคือสมมติฐานเริ่มต้น - ข้อยกเว้นจะไม่เพิ่มขึ้น

หากคุณไม่พูดอะไรเลยนั่นจะถือว่าเป็นการทดสอบทุกครั้ง

คุณไม่จำเป็นต้องเขียนคำยืนยันใด ๆ


7
@IndradhanushGupta คำตอบที่ยอมรับทำให้การทดสอบแบบ pythonic ดีกว่าคำตอบนี้ ชัดเจนดีกว่าโดยปริยาย
0xc0de

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

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

4
@federicojasson คุณตอบคำถามของคุณเองได้ค่อนข้างดีในประโยคที่สอง ข้อผิดพลาดกับความล้มเหลวในการทดสอบสามารถอธิบายได้อย่างกระชับว่า "เกิดปัญหาที่ไม่คาดคิด" และ "พฤติกรรมที่ไม่ตั้งใจ" ตามลำดับ คุณต้องการให้การทดสอบของคุณแสดงข้อผิดพลาดเมื่อฟังก์ชั่นของคุณล้มเหลว แต่ไม่ใช่เมื่อมีข้อยกเว้นที่คุณรู้ว่ามันจะส่งออกไป
coredumperror

52

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

sValidPath=AlwaysSuppliesAValidPath()
# Check PathIsNotAValidOne not thrown
MyObject(sValidPath)

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

1
การทดสอบของคุณผ่านหรือไม่ ถ้ามันไม่ผ่านคุณจะต้องไปแก้ไข ไม่ว่าจะเป็นการรายงานว่าเป็น "ความล้มเหลว" หรือ "ข้อผิดพลาด" นั้นส่วนใหญ่ไม่เกี่ยวข้อง มีความแตกต่างอย่างหนึ่ง: ด้วยคำตอบของฉันคุณจะเห็นการติดตามสแต็กเพื่อให้คุณเห็นว่า PathIsNotAValidOne ถูกส่งไปที่ไหน ด้วยคำตอบที่ยอมรับคุณจะไม่มีข้อมูลนั้นดังนั้นการดีบั๊กจะยากขึ้น (สมมติว่า Py2 ไม่แน่ใจว่า Py3 นั้นดีกว่านี้หรือไม่)
user9876

19
@ user9876 - ไม่ เงื่อนไขการออกจากการทดสอบคือ 3 (pass / nopass / error) ไม่ใช่ 2 เนื่องจากคุณเชื่อผิดพลาด ความแตกต่างระหว่างข้อผิดพลาดและความล้มเหลวนั้นเป็นเรื่องสำคัญและปฏิบัติต่อพวกเขาราวกับว่าพวกมันเหมือนกันคือโปรแกรมที่ไม่ดี หากคุณไม่เชื่อฉันลองดูว่านักวิ่งทดสอบทำงานอย่างไรและต้นไม้ตัดสินใจใดที่นำไปใช้กับความล้มเหลวและข้อผิดพลาด จุดเริ่มต้นที่ดีสำหรับไพ ธ อนคือตัวxfailตกแต่งใน pytest
mac

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

1
@ user9876 ความแตกต่างระหว่าง 'ล้มเหลว' และ 'ข้อผิดพลาด' คือความแตกต่างระหว่าง "ยืนยันของฉันล้มเหลว" และ "การทดสอบของฉันไม่ได้รับการยืนยัน" สำหรับฉันแล้วมันเป็นความแตกต่างที่มีประโยชน์ระหว่างการแก้ไขการทดสอบ แต่ฉันเดาตามที่คุณพูดไม่ใช่สำหรับทุกคน
CS

14

ฉันเป็นโปสเตอร์ดั้งเดิมและฉันยอมรับคำตอบข้างต้นโดย DGH โดยไม่ต้องใช้มันเป็นครั้งแรกในรหัส

เมื่อฉันใช้ฉันรู้ว่ามันจำเป็นต้องมีการปรับแต่งเล็กน้อยเพื่อทำสิ่งที่ฉันต้องการ (เพื่อให้เกิดความยุติธรรมกับ DGH เขา / เธอพูดว่า "หรืออะไรทำนองนั้น"!)

ฉันคิดว่ามันคุ้มค่าที่จะโพสต์บิดที่นี่เพื่อประโยชน์ของผู้อื่น:

    try:
        a = Application("abcdef", "")
    except pySourceAidExceptions.PathIsNotAValidOne:
        pass
    except:
        self.assertTrue(False)

สิ่งที่ฉันพยายามทำที่นี่คือเพื่อให้แน่ใจว่าหากมีความพยายามในการสร้างอินสแตนซ์ของแอปพลิเคชันวัตถุด้วยอาร์กิวเมนต์ที่สองของช่องว่าง pySourceAidExceptions.PathIsNotAValidOne จะยกขึ้น

ฉันเชื่อว่าการใช้รหัสด้านบน (ตามคำตอบของ DGH อย่างมาก) จะทำเช่นนั้น


2
เนื่องจากคุณกำลังชี้แจงคำถามและไม่ตอบคำถามคุณควรแก้ไข (ไม่ตอบ) โปรดดูคำตอบของฉันด้านล่าง
hiwaylon

13
ดูเหมือนว่าจะตรงกันข้ามกับปัญหาดั้งเดิม self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath)ควรทำงานในกรณีนี้
Antony Hatchkins

8

คุณสามารถกำหนดassertNotRaisesโดยการนำประมาณ 90% ของการดำเนินงานเดิมของassertRaisesในunittestโมดูล ด้วยวิธีนี้คุณท้ายด้วยวิธีการที่นอกเหนือจากสภาพความล้มเหลวกลับมีพฤติกรรมเหมือนกันกับassertNotRaisesassertRaises

TLDR และการสาธิตสด

มันกลับกลายเป็นassertNotRaisesวิธีที่ง่ายอย่างน่าประหลาดใจในการเพิ่มวิธีการunittest.TestCase(ใช้เวลาประมาณ 4 ครั้งในการเขียนคำตอบนี้ตามที่ใช้รหัส) นี่คือการสาธิตสดของassertNotRaisesวิธีการในการดำเนินการ เพียงเช่นassertRaisesคุณสามารถผ่าน callable และ args ไปassertNotRaisesหรือคุณสามารถใช้มันในwithคำสั่ง การสาธิตสดรวมถึงกรณีทดสอบที่แสดงให้เห็นว่าassertNotRaisesทำงานได้ตามที่ต้องการ

รายละเอียด

การนำไปใช้assertRaisesในunittestค่อนข้างซับซ้อน แต่ด้วย subclassing เล็กน้อยคุณสามารถแทนที่และย้อนกลับเงื่อนไขความล้มเหลว

assertRaisesเป็นวิธีสั้น ๆ ที่โดยทั่วไปแล้วสร้างอินสแตนซ์ของunittest.case._AssertRaisesContextคลาสและส่งคืนมัน (ดูคำจำกัดความของมันในunittest.caseโมดูล) คุณสามารถกำหนด_AssertNotRaisesContextคลาสของคุณเองโดยการแบ่งคลาสย่อย_AssertRaisesContextและแทนที่__exit__เมธอด:

import traceback
from unittest.case import _AssertRaisesContext

class _AssertNotRaisesContext(_AssertRaisesContext):
    def __exit__(self, exc_type, exc_value, tb):
        if exc_type is not None:
            self.exception = exc_value.with_traceback(None)

            try:
                exc_name = self.expected.__name__
            except AttributeError:
                exc_name = str(self.expected)

            if self.obj_name:
                self._raiseFailure("{} raised by {}".format(exc_name,
                    self.obj_name))
            else:
                self._raiseFailure("{} raised".format(exc_name))

        else:
            traceback.clear_frames(tb)

        return True

TestCaseปกติคุณกำหนดชั้นเรียนกรณีทดสอบโดยมีพวกเขาสืบทอดมาจาก หากคุณได้รับมรดกจากคลาสย่อยแทนMyTestCase:

class MyTestCase(unittest.TestCase):
    def assertNotRaises(self, expected_exception, *args, **kwargs):
        context = _AssertNotRaisesContext(expected_exception, self)
        try:
            return context.handle('assertNotRaises', args, kwargs)
        finally:
            context = None

กรณีทดสอบทั้งหมดของคุณตอนนี้จะมีassertNotRaisesวิธีการที่ใช้ได้


อยู่ที่ไหนของคุณtracebackในของคุณelseคำสั่งมาจากไหน
NOhs

1
@NOhs importมีขาดหายไปคือ คงที่
โทร

2
def _assertNotRaises(self, exception, obj, attr):                                                                                                                              
     try:                                                                                                                                                                       
         result = getattr(obj, attr)                                                                                                                                            
         if hasattr(result, '__call__'):                                                                                                                                        
             result()                                                                                                                                                           
     except Exception as e:                                                                                                                                                     
         if isinstance(e, exception):                                                                                                                                           
            raise AssertionError('{}.{} raises {}.'.format(obj, attr, exception)) 

สามารถแก้ไขได้หากคุณต้องยอมรับพารามิเตอร์

โทรเหมือน

self._assertNotRaises(IndexError, array, 'sort')

1

ฉันคิดว่ามันมีประโยชน์กับ Monkey-patch unittestดังนี้:

def assertMayRaise(self, exception, expr):
  if exception is None:
    try:
      expr()
    except:
      info = sys.exc_info()
      self.fail('%s raised' % repr(info[0]))
  else:
    self.assertRaises(exception, expr)

unittest.TestCase.assertMayRaise = assertMayRaise

สิ่งนี้จะอธิบายถึงเจตนาเมื่อทำการทดสอบโดยไม่มีข้อยกเว้น:

self.assertMayRaise(None, does_not_raise)

สิ่งนี้ช่วยลดความยุ่งยากในการทดสอบในวงซึ่งฉันมักจะพบว่าตัวเองกำลังทำ:

# ValueError is raised only for op(x,x), op(y,y) and op(z,z).
for i,(a,b) in enumerate(itertools.product([x,y,z], [x,y,z])):
  self.assertMayRaise(None if i%4 else ValueError, lambda: op(a, b))

แพทช์ลิงคืออะไร?
ScottMcC

1
ดูen.wikipedia.org/wiki/Monkey_patch หลังจากเพิ่มassertMayRaiseไปunittest.TestSuiteคุณก็สามารถแสร้งทำเป็นว่ามันเป็นส่วนหนึ่งของunittestห้องสมุด
AndyJost

0

หากคุณผ่านคลาส Exception ไปที่จะassertRaises()มีการจัดการบริบท สิ่งนี้สามารถปรับปรุงความสามารถในการอ่านของการทดสอบของคุณ:

# raise exception if Application created with bad data
with self.assertRaises(pySourceAidExceptions.PathIsNotAValidOne):
    application = Application("abcdef", "")

สิ่งนี้ช่วยให้คุณสามารถทดสอบกรณีข้อผิดพลาดในรหัสของคุณ

ในกรณีนี้คุณกำลังทดสอบการPathIsNotAValidOneยกเมื่อคุณส่งพารามิเตอร์ที่ไม่ถูกต้องไปที่ตัวสร้างแอปพลิเคชัน


1
ไม่สิ่งนี้จะล้มเหลวก็ต่อเมื่อไม่มีการยกข้อยกเว้นภายในบล็อกตัวจัดการบริบท สามารถทดสอบได้อย่างง่ายดายโดย 'with self.assertRaises (TypeError): ยก TypeError' ซึ่งผ่าน
Matthew Trevor

@MatthewTrevor โทรดีมาก ในขณะที่ฉันจำได้ว่าแทนที่จะทดสอบรหัสดำเนินการอย่างถูกต้องนั่นคือไม่เพิ่มขึ้นฉันแนะนำการทดสอบกรณีข้อผิดพลาด ฉันแก้ไขคำตอบแล้ว หวังว่าฉันจะได้รับ +1 เพื่อรับของสีแดง :)
hiwaylon

โปรดทราบว่านี่เป็น Python 2.7 และใหม่กว่า: docs.python.org/2/library/…
qneill

0

คุณสามารถลองเช่นนั้น ลอง: self.assertRaises (ไม่มี, ฟังก์ชั่น, arg1, arg2) ยกเว้น: ส่งถ้าคุณไม่ใส่รหัสในลองบล็อกมันจะผ่านข้อยกเว้น 'AssertionError: ไม่มีไม่ยก "และกรณีทดสอบจะล้มเหลวกรณีทดสอบจะผ่าน ถ้าใส่ในบล็อกลองซึ่งเป็นพฤติกรรมที่คาดหวัง


0

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

นี่คือตัวอย่าง:

p = SomeClass(param1=_param1_value)
self.assertTrue(isinstance(p, SomeClass))
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.