Asserting an expected exception is easy with MbUnit. Most of the time, the simple use of
ExpectedExceptionAttribute
(and similar) is sufficient:[Test]
[ExpectedArgumentNullException]
public void DoSomething_with_null_argument_should_throw_exception()
{
var foo = new Foo();
foo.DoSomething(null);
}
This is a very handy solution. But it does not come without any inconvenient:
- Even if the test passes, you cannot be sure that the exception was thrown exactly where it was expected to be thrown. It is possible that another portion of the code has thrown an exception of the same type, before the expected failure point was reached. For example, a bug in the
Foo
constructor could have thrown the same exception; and it would make your test completely irrelevant because it would pass for a wrong reason. - You cannot perform any additional assertion about the exception itself and its properties, like the message, or the inner exception.
Assert.Throws
method, which solves all those limitations. It is very easy to use, in particular with the lambda expression syntax.[Test]
public void DoSomething_with_null_argument_should_throw_exception()
{
var foo = new Foo();
Assert.Throws<ArgumentNullException>(() => foo.DoSomething(null));
}
Remark that
ExpectedArgumentNullExceptionAttribute
is no longer needed. Now, you are sure that the exception was thrown when passing a null reference to the DoSomething
method, and not before, in an obscure part of the Foo
constructor.But wait! you can do even more! Unlike most of the other MbUnit assertion methods which just return void, Assert.Throws
returns the instance of the exception which was caught. With that instance, you can make some more interesting assertions:[Test]
public void DoSomething_with_null_argument_should_throw_exception()
{
var foo = new Foo();
var exception = Assert.Throws<ArgumentNullException>(() => foo.DoSomething(null));
Assert.AreEqual("Detected breach in space-time continuum.", exception.Message);
}
This is particularly useful for parameterized tests based on reflection. Indeed, reflective invocations encapsulate the exceptions into a
TargetInvocationException
instance, which makes the reason for the error a bit less discoverable:public Foo2
{
public void SetInt32(int value) { // ... }
public void SetDouble(double value) { // ... }
public void SetInt16(short value) { // ... }
}
[Test]
[Row(typeof(int), 9999, "SetInt32")]
[Row(typeof(double), 9999.9, "SetDouble")]
[Row(typeof(short), 9999, "SetInt16")]
public void Set_too_large_value_should_throw_exception<T>(T tooLargeValue, string methodName)
{
var foo2 = new Foo2();
var method = typeof(Foo2).GetMethod(methodName);
var exception = Assert.Throws<TargetInvocationException>(() => method.Invoke(foo2, new object[] { tooLargeValue }));
Assert.IsInstanceOfType<ArgumentOutOfRangeException>(exception.InnerException);
}
No comments:
Post a Comment