Monday, March 21, 2011

Testing for Exceptions and Expectations

One of the things that I frequently have need to do is make sure that my methods throw proper exceptions when exceptional conditions occur.  .NET unit testing frameworks support typically support this using attributes.  For example:

[Test]
[ExpectedException(typeof(ArgumentNullException))]
public void ShouldThrowAnArgumentNullExceptionWhenANullParameterIsSupplied()
{
    var foo = new Foo();
    
    foo.Bar( null );
}

This works wonders for simple cases, but what about those cases where I want to make sure that some expectations are met.  Because the verification code comes after the invocation of the method under test, it won’t get run when an exception is thrown.  One way around this is to set up your mock objects and verify your expectations in your test initialize/cleanup methods.  This works because the cleanup code is run even for tests that throw exceptions.

private Bar MockBar { get; set; }

[TestInitialize]
public void MyTestInitialize()
{
    this.MockBar = MockRepository.GenerateMock<Bar>();
}

[TestCleanup]
public void  MyTestCleanup()
{
    this.MockBar.VerifyAllExpectations();
}

[Test]
public void ShouldThrowAnArgumentNullExceptionWhenANullParameterIsSupplied()
{
    // Baz should never be called when Bar is passed a null parameter
    this.MockBar.Expect( b => b.Baz() ).Repeat.Never();

    var foo = new Foo();

    foo.Bar( null );  
}
This works well when your tests share similar dependencies.  You could even extend this so that each test sets up its own or uses shared private methods to set up groups of expectations and the verification step checks to make sure that a mock object exists before it verifies it’s expectations.  This is a valid way solve the problem, but one I’ve found that can get excessively complicated and comes with code smells (you end up with a fair amount of conditional logic in the cleanup code).

An alternative to this is to not use the ExpectedException attribute, but to build the logic of catching and validating the exception into your test.

[Test]
public void ShouldThrowAnArgumentNullExceptionWhenANullParameterIsSupplied()
{
    var mockBar = MockRepository.GenerateMock<Bar>();

    // Baz should never be called when Bar is passed a null parameter
    mockBar.Expect( b => b.Baz() ).Repeat.Never();

    var foo = new Foo();

    try
    {
        foo.Bar( null );
        throw new AssertFailedException(); // test fails due to no exception thrown
    }
    catch (ArgumentNullException) // only catch the one exception we expect
    {
    }

    mockBar.VerifyAllExpectations();   
}

This is, in a word, ugly. It has the advantage of flexibility and keeping the expectation code specific and local to the current test. You could also, if you wanted, refactor to share expectation set up/verification between tests. There's still a code smell here: you're repeating a pattern whenever you want this type of test, but I think it's more in keeping with the single responsibility principle since you're cleanup code really is only concerned with clean up that necessarily affects all tests and isn't being abused to (selectively) run verification on each test's mocks. If only we could make it a little less ugly by encapsulating the pattern. Voila! A couple of helper classes to the rescue!

public class AssertionFailedException : Exception
{
    public AssertionFailedException() : base() { }
    public AssertionFailedException( string message )
        : base( message ) { }
}

public static class AssertException
{
    public static void IsThrown<T>( Action action ) where T : Exception
    {
        try
        {
            action();
            throw new AssertionFailedException( string.Format( "An expected exception of type {0} was not thrown", typeof(T).Name ) );
        }
        catch (T)
        {
        }
    }
}

Now we have a much cleaner way to assert that an exception has been thrown without polluting our clean up methods. In cases where it isn't appropriate, because our tests don’t share a lot of dependencies, to use the test initialization/cleanup methods to create and enforce our expecations, this mechanism works very well. As usual, you are still able to refactor your setup/verification for dependent objects as needed for tests that use the same code.

[Test]
public void ShouldThrowAnArgumentNullExceptionWhenANullParameterIsSupplied()
{
    var mockBar = MockRepository.GenerateMock<Bar>();

    // Baz should never be called when Bar is passed a null parameter
    mockBar.Expect( b => b.Baz() ).Repeat.Never();

    var foo = new Foo();

    AssertException.IsThrown<ArgumentNullException>( () => foo.Bar( null ) );

    mockBar.VerifyAllExpectations();   
}

No comments :

Post a Comment

Comments are moderated.