Sunday, August 24, 2008

Unit Tests & Expected Exceptions

Good unit test coverage should include tests that cover the normal use cases in your code, and tests that validate any bounds are being checked. What does this mean? Take for example the following function.


public File OpenFile(string filePath)

{

Check.Require(!string.IsNullOrEmpty(filePath),"Path cannot be null or empty");

Check.Require(File.Exists(filePath), "Path must exist");

...

}




A good unit test suite will ensure that the code handles the exception cases, when the codes pre & post conditions are not met. When writting these tests you will need to watch for exceptions. One quick implementation of this is:

try

{

FileInfo file = OpenFile(null);

}

catch (PreconditionException ex){}

catch (Exception badEx)

{

Assert.Fail();

}



While this works it suffers from a critical problem that how do I know which exception we were expecting to get? Unless this is documented, I can't be certain of the original test writters intentions.

Many of the unit testing frameworks provide a solution to this with an ExpectedException (or some variant there of) attribute which can be placed on a test method. This attribute typically takes a type of exception and optionally a message that should be the message of the exception.

This is definetly better and results in a simple test in the MS Test framework such as the following:


[TestMethod]

[ExpectedException(typeof(PreconditionException))]

public void OpenFile_NullPathTest()

{

FileInfo file = OpenFile(null);

}



This works fine in this simple case, but what if we have a more complex test, or we are doign an integration test and testing lots of components. If any component throws the exception then your test will still pass.

A better way would let us both clearly express the intent of the test, and to narrow down a specific line or set of lines that should generate the exception.

In C# 3.0 we can leverage delgates and extension methods to come up with a simple way that achieves these goals. Our test method now looks like:



[TestMethod]

public void OpenFile_NullPathTest()

{

Action action=delegate{ OpenFile(null); };

action.ExpectedException(typeof(PreconditionException));

}




This test clearly states that the OpenFile(null) call will result in a PreconditionException, and if there is other stuff occuring within the test, we don't have to worry about the test passing when it should have passed.

The ExpectedException method is an extension method on an Action Delegate. Action is a general delegate that takes no arguments and returns a void. To use this you need to add the following method to a static class, and make sure that the class is within a referenced name space (using in C# or imports in vb).



public static void ExpectedException(this Action action, Type expectedException)

{

try

{

action();

Assert.Fail("Expected exception did not occur");

}

catch (Exception ex)

{

Assert.AreEqual(expectedException, ex.GetType(), string.Format("Expected exception of type {0} but recieved exception of {1}", expectedException.Name, ex.GetType().Name));

}

}



This method could be enhanced to allow checking that the right exception is thrown but that the message is what you expect; however, I'll leave that as an exercise for you.

If your not a big fan of extension methods you could do this using a delegate or even nicer with a lamba such as:
TestHelper.ExpectedException(() => OpenFile(null),typeof(PreconditionException));

Enjoy!

Josh

Edited: 11-17-2008 Added an Assert.Fail() after we call the action delegate in our test helper.

4 comments:

Anonymous said...

Nice! I like the simple use of delegates to allow multiple method calls to be grouped in a single test method and checked one after the other.

Josh Berke said...

Glad you liked it

Hainesy said...
This comment has been removed by the author.
Hainesy said...

Thanks for this. I adapted it slightly to use a generic syntax, so it can be called like:

action.ExpectedException<MyException>();

Code as follows:

public static void Expect<T>(this Action action) where T : Exception
{
try
{
action();
Assert.Fail(string.Format("Expected exception of type {0} did not occur", typeof(T).Name));
}
catch (AssertionException ex)
{
throw;
}
catch (Exception ex)
{
var exType = ex.GetType();
if (exType != typeof(T))
{
Assert.Fail(string.Format("Expected exception of type {0} but recieved {1}", typeof(T).Name, exType.Name));
}
}
}