วิธีที่ดีที่สุดในการทดสอบข้อยกเว้นด้วย Assert เพื่อให้แน่ใจว่าจะถูกโยนทิ้ง


99

คุณคิดว่านี่เป็นวิธีที่ดีสำหรับการทดสอบข้อยกเว้นหรือไม่? ข้อเสนอแนะใด ๆ ?

Exception exception = null;
try{
    //I m sure that an exeption will happen here
}
catch (Exception ex){
    exception = ex;
}

Assert.IsNotNull(exception);

ฉันใช้ MS Test

คำตอบ:


139

ฉันมีรูปแบบที่แตกต่างกันสองสามแบบที่ฉันใช้ ฉันใช้ExpectedExceptionแอตทริบิวต์เกือบตลอดเวลาเมื่อคาดว่าจะมีข้อยกเว้น สิ่งนี้เพียงพอสำหรับกรณีส่วนใหญ่อย่างไรก็ตามมีบางกรณีที่ไม่เพียงพอ ข้อยกเว้นอาจไม่สามารถจับได้ - เนื่องจากถูกโยนโดยวิธีการที่เรียกใช้โดยการสะท้อน - หรือบางทีฉันแค่ต้องการตรวจสอบว่ามีเงื่อนไขอื่น ๆ อยู่กล่าวว่าธุรกรรมถูกย้อนกลับหรือยังคงมีการตั้งค่าบางอย่าง ในกรณีเหล่านี้ฉันห่อไว้ในtry/catchบล็อกที่คาดว่าจะมีข้อยกเว้นที่แน่นอนทำAssert.Failถ้ารหัสสำเร็จและตรวจจับข้อยกเว้นทั่วไปเพื่อให้แน่ใจว่าไม่มีข้อยกเว้นอื่น

กรณีแรก:

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void MethodTest()
{
     var obj = new ClassRequiringNonNullParameter( null );
}

กรณีที่สอง:

[TestMethod]
public void MethodTest()
{
    try
    {
        var obj = new ClassRequiringNonNullParameter( null );
        Assert.Fail("An exception should have been thrown");
    }
    catch (ArgumentNullException ae)
    {
        Assert.AreEqual( "Parameter cannot be null or empty.", ae.Message );
    }
    catch (Exception e)
    {
        Assert.Fail(
             string.Format( "Unexpected exception of type {0} caught: {1}",
                            e.GetType(), e.Message )
        );
    }
}

16
กรอบการทดสอบหลายหน่วยใช้ความล้มเหลวในการยืนยันเป็นข้อยกเว้น ดังนั้น Assert.Fail () ในกรณีที่สองจะถูกจับโดยบล็อก catch (Exception) ซึ่งจะซ่อนข้อความข้อยกเว้น คุณต้องเพิ่มการจับ (NUnit.Framework.AssertionException) {throw;} หรือคล้ายกัน - ดูคำตอบของฉัน
GrahamS

@Graham - ฉันพิมพ์สิ่งนี้ออกจากด้านบนของหัว โดยปกติฉันจะพิมพ์ข้อความยกเว้นนอกเหนือจากประเภท ประเด็นคือการทดสอบจะล้มเหลวเนื่องจากตัวจัดการที่สองตรวจพบข้อผิดพลาดในการยืนยันและ "refail" พร้อมข้อมูลเกี่ยวกับข้อผิดพลาด
tvanfosson

1
แม้ว่าโค้ดของคุณจะฟังดูใช้งานได้ แต่ฉันไม่แนะนำให้ใช้แอตทริบิวต์ที่คาดหวังได้ (เนื่องจากมีข้อ จำกัด และเกิดข้อผิดพลาดมากเกินไป) หรือเขียนบล็อก try / catch ในการทดสอบแต่ละครั้ง (เนื่องจากซับซ้อนเกินไปและเกิดข้อผิดพลาดได้ง่าย) ใช้วิธีการยืนยันที่ได้รับการออกแบบมาเป็นอย่างดีไม่ว่าจะจัดทำโดยกรอบการทดสอบของคุณหรือเขียนขึ้นเอง คุณสามารถบรรลุรหัสที่ดีขึ้นและคุณไม่จำเป็นต้องเลือกระหว่างเทคนิคต่างๆหรือเปลี่ยนจากเทคนิคหนึ่งไปยังอีกเทคนิคหนึ่งเมื่อการทดสอบเปลี่ยนไป ดูstackoverflow.com/a/25084462/2166177
steve

FYI - ฉันได้เปลี่ยนไปใช้ xUnit ซึ่งมีAssert.Throwsวิธีการพิมพ์ ที่ชัดเจนซึ่งครอบคลุมทั้งสองกรณีนี้
tvanfosson

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

47

ตอนนี้ปี 2017 คุณสามารถทำได้ง่ายขึ้นด้วยMSTest V2 Framework ใหม่ :

Assert.ThrowsException<Exception>(() => myClass.MyMethodWithError());

//async version
await Assert.ThrowsExceptionAsync<SomeException>(
  () => myObject.SomeMethodAsync()
);

สิ่งนี้จะสำเร็จก็ต่อเมื่อมีการSystem.Exceptionโยน อื่น ๆ เช่นSystem.ArgumentExceptionจะล้มเหลวในการทดสอบ
sschoof

2
หากคุณคาดหวังข้อยกเว้นประเภทอื่นคุณควรทดสอบ ... ในตัวอย่างของคุณคุณ shoud do: Assert.ThrowsException <ArgumentException> (() => myClass.MyMethodWithError ());
Icaro Bombonato

3
สิ่งสำคัญที่ควรทราบคือการใช้Assert.ThrowsException<MyException>จะทดสอบเฉพาะกับประเภทข้อยกเว้นที่ให้ไว้เท่านั้นและไม่ใช่ประเภทข้อยกเว้นที่ได้รับใด ๆ ในตัวอย่างของฉันหากผ่านการทดสอบSubก็จะ(ชนิดที่ได้มาจากชั้นฐาน) แล้วการทดสอบจะล้มเหลว ThrowMyInheritedExceptionMyException
อาม่า

หากคุณต้องการขยายการทดสอบของคุณและยอมรับประเภทข้อยกเว้นรวมถึงประเภทที่ได้รับมาให้ใช้กTry { SubToTest(); Assert.Fail("...") } Catch (AssertFailedException e) {throw;} Catch (MyException e) {...}. สังเกตความสำคัญสูงสุดของCatch (AssertFailedException e) {throw;}(เปรียบเทียบความคิดเห็นจาก allgeek)
Ama

16

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

try
{
    SomethingThatCausesAnException();
    Assert.Fail("Should have exceptioned above!");
}
catch (Exception ex)
{
    // whatever logging code
}

ในกรอบการทดสอบหน่วยทั้งหมดที่ฉันคุ้นเคยAssert.Failทำงานโดยทิ้งข้อยกเว้นดังนั้นการจับทั่วไปจะปกปิดความล้มเหลวของการทดสอบ หากSomethingThatCausesAnException()ไม่โยนAssert.Failเจตจำนง แต่จะไม่มีวันส่งผลให้นักวิ่งทดสอบแสดงถึงความล้มเหลว

หากคุณต้องการตรวจจับข้อยกเว้นที่คาดไว้ (กล่าวคือเพื่อยืนยันรายละเอียดบางอย่างเช่นข้อความ / คุณสมบัติในข้อยกเว้น) สิ่งสำคัญคือต้องจับประเภทที่คาดหวังที่เฉพาะเจาะจงไม่ใช่คลาส Exception พื้นฐาน นั่นจะช่วยให้Assert.Failข้อยกเว้นเกิดฟอง (สมมติว่าคุณไม่ได้โยนข้อยกเว้นประเภทเดียวกับที่กรอบการทดสอบหน่วยของคุณทำ) แต่ยังคงอนุญาตให้มีการตรวจสอบความถูกต้องตามข้อยกเว้นที่SomethingThatCausesAnException()วิธีการของคุณโยนทิ้งไป


16

ตั้งแต่ v 2.5 NUnitมีระดับวิธีการต่อไปนี้Assertสำหรับการทดสอบข้อยกเว้น:

Assert.Throwsซึ่งจะทดสอบประเภทข้อยกเว้นที่แน่นอน:

Assert.Throws<NullReferenceException>(() => someNullObject.ToString());

และAssert.Catchซึ่งจะทดสอบข้อยกเว้นของประเภทที่กำหนดหรือประเภทข้อยกเว้นที่ได้รับจากประเภทนี้:

Assert.Catch<Exception>(() => someNullObject.ToString());

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

แก้ไข

เพียงเพื่อยกตัวอย่างความคิดเห็นของ Matthew ด้านล่างการกลับมาของ generic Assert.ThrowsและAssert.Catchเป็นข้อยกเว้นของประเภทของข้อยกเว้นซึ่งคุณสามารถตรวจสอบเพื่อตรวจสอบเพิ่มเติมได้:

// The type of ex is that of the generic type parameter (SqlException)
var ex = Assert.Throws<SqlException>(() => MethodWhichDeadlocks());
Assert.AreEqual(1205, ex.Number);

2
Roy Osherove แนะนำสิ่งนี้ใน The Art of Unit Testing, second ed, section 2.6.2
Avi

2
ฉันชอบAssert.Throwsนอกจากนี้ยังส่งคืนข้อยกเว้นเพื่อให้คุณสามารถเขียนคำยืนยันเพิ่มเติมเกี่ยวกับข้อยกเว้นได้
Matthew

คำถามคือ MSTest ไม่ใช่ NUnit
bytedev

คำถามเดิมของ @nashwan OP ไม่มีคุณสมบัตินั้นและการติดแท็กยังไม่เข้าเกณฑ์ MS-Test มันเป็นคำถาม C #, .Net, Unit-Testing
StuartLC

13

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

เมื่อฉันใช้ (/ บังคับโดยไคลเอนต์) เพื่อใช้ MSTest ฉันจะใช้คลาสตัวช่วยนี้เสมอ:

public static class AssertException
{
    public static void Throws<TException>(Action action) where TException : Exception
    {
        try
        {
            action();
        }
        catch (Exception ex)
        {
            Assert.IsTrue(ex.GetType() == typeof(TException), "Expected exception of type " + typeof(TException) + " but type of " + ex.GetType() + " was thrown instead.");
            return;
        }
        Assert.Fail("Expected exception of type " + typeof(TException) + " but no exception was thrown.");
    }

    public static void Throws<TException>(Action action, string expectedMessage) where TException : Exception
    {
        try
        {
            action();
        }
        catch (Exception ex)
        {
            Assert.IsTrue(ex.GetType() == typeof(TException), "Expected exception of type " + typeof(TException) + " but type of " + ex.GetType() + " was thrown instead.");
            Assert.AreEqual(expectedMessage, ex.Message, "Expected exception with a message of '" + expectedMessage + "' but exception with message of '" + ex.Message + "' was thrown instead.");
            return;
        }
        Assert.Fail("Expected exception of type " + typeof(TException) + " but no exception was thrown.");
    }
}

ตัวอย่างการใช้งาน:

AssertException.Throws<ArgumentNullException>(() => classUnderTest.GetCustomer(null));

10

อีกทางเลือกหนึ่งในการใช้ExpectedExceptionแอตทริบิวต์บางครั้งฉันกำหนดวิธีการที่เป็นประโยชน์สองวิธีสำหรับชั้นเรียนทดสอบของฉัน

AssertThrowsException() รับผู้รับมอบสิทธิ์และยืนยันว่าจะส่งข้อยกเว้นที่คาดไว้พร้อมกับข้อความที่คาดไว้

AssertDoesNotThrowException() ใช้ผู้รับมอบสิทธิ์คนเดียวกันและยืนยันว่าไม่มีข้อยกเว้น

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

การใช้รหัสทดสอบหน่วยของฉันอาจมีลักษณะดังนี้:

ExceptionThrower callStartOp = delegate(){ testObj.StartOperation(); };

// Check exception is thrown correctly...
AssertThrowsException(callStartOp, typeof(InvalidOperationException), "StartOperation() called when not ready.");

testObj.Ready = true;

// Check exception is now not thrown...
AssertDoesNotThrowException(callStartOp);

สวยและเรียบร้อยเหรอ?

My AssertThrowsException()และAssertDoesNotThrowException()method ถูกกำหนดบนคลาสพื้นฐานทั่วไปดังนี้:

protected delegate void ExceptionThrower();

/// <summary>
/// Asserts that calling a method results in an exception of the stated type with the stated message.
/// </summary>
/// <param name="exceptionThrowingFunc">Delegate that calls the method to be tested.</param>
/// <param name="expectedExceptionType">The expected type of the exception, e.g. typeof(FormatException).</param>
/// <param name="expectedExceptionMessage">The expected exception message (or fragment of the whole message)</param>
protected void AssertThrowsException(ExceptionThrower exceptionThrowingFunc, Type expectedExceptionType, string expectedExceptionMessage)
{
    try
    {
        exceptionThrowingFunc();
        Assert.Fail("Call did not raise any exception, but one was expected.");
    }
    catch (NUnit.Framework.AssertionException)
    {
        // Ignore and rethrow NUnit exception
        throw;
    }
    catch (Exception ex)
    {
        Assert.IsInstanceOfType(expectedExceptionType, ex, "Exception raised was not the expected type.");
        Assert.IsTrue(ex.Message.Contains(expectedExceptionMessage), "Exception raised did not contain expected message. Expected=\"" + expectedExceptionMessage + "\", got \"" + ex.Message + "\"");
    }
}

/// <summary>
/// Asserts that calling a method does not throw an exception.
/// </summary>
/// <remarks>
/// This is typically only used in conjunction with <see cref="AssertThrowsException"/>. (e.g. once you have tested that an ExceptionThrower
/// method throws an exception then your test may fix the cause of the exception and then call this to make sure it is now fixed).
/// </remarks>
/// <param name="exceptionThrowingFunc">Delegate that calls the method to be tested.</param>
protected void AssertDoesNotThrowException(ExceptionThrower exceptionThrowingFunc)
{
    try
    {
        exceptionThrowingFunc();
    }
    catch (NUnit.Framework.AssertionException)
    {
        // Ignore and rethrow any NUnit exception
        throw;
    }
    catch (Exception ex)
    {
        Assert.Fail("Call raised an unexpected exception: " + ex.Message);
    }
}

4

ด้วยกรอบการทดสอบหน่วย. net ส่วนใหญ่คุณสามารถใส่แอตทริบิวต์ [expectedException] ในวิธีการทดสอบได้ อย่างไรก็ตามสิ่งนี้ไม่สามารถบอกคุณได้ว่าข้อยกเว้นเกิดขึ้นในจุดที่คุณคาดหวังไว้ นั่นคือสิ่งที่xunit.netสามารถช่วยได้

ด้วย xunit คุณมี Assert.Throws เพื่อให้คุณสามารถทำสิ่งต่างๆดังนี้:

    [Fact]
    public void CantDecrementBasketLineQuantityBelowZero()
    {
        var o = new Basket();
        var p = new Product {Id = 1, NetPrice = 23.45m};
        o.AddProduct(p, 1);
        Assert.Throws<BusinessException>(() => o.SetProductQuantity(p, -3));
    }

[Fact] เท่ากับ xunit ของ [TestMethod]


หากคุณต้องใช้ MSTest (ซึ่งฉันมักถูกนายจ้างบังคับ) ให้ดูคำตอบของฉันด้านล่าง
bytedev

4

ทำเครื่องหมายการทดสอบด้วย expectedExceptionAttribute (นี่คือคำศัพท์ใน NUnit หรือ MSTest ผู้ใช้เฟรมเวิร์กการทดสอบหน่วยอื่น ๆ อาจต้องแปล)


อย่าใช้ที่คาดหวัง NUnit มี Assert.Throws <YourException> () และสำหรับ MSTest ให้ใช้บางอย่างเช่นคลาส AssertException ของฉันด้านล่าง
bytedev

0

แนะนำให้ใช้ไวยากรณ์ตัวแทนที่สะอาดของNUnit

ตัวอย่างสำหรับการทดสอบ ArgumentNullExeption :

[Test]
[TestCase(null)]
public void FooCalculation_InvalidInput_ShouldThrowArgumentNullExeption(string text)
{
    var foo = new Foo();
    Assert.That(() => foo.Calculate(text), Throws.ArgumentNullExeption);

    //Or:
    Assert.That(() => foo.Calculate(text), Throws.Exception.TypeOf<ArgumentNullExeption>);
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.