Handling Exception in BDD-style Tests

(I originally posted this on my MSDN blog.)

Exceptions cause problems with BDD-style tests in the MSTest environment.  The MSTest [ExpectedException] doesn’t work well in this case since the exception will be thrown in the BecauseOf() method, which MSTest considers to be part of the initialization, so all tests in the context will fail.  The best way to deal with expected exceptions in BDD-style tests is to catch and save the exception when you perform the action in BecauseOf(), and then check the attributes of the saved exception in your tests.

Here’s an example of how we observe exceptions.  It’s not particularly important to understand what the class under test does, but briefly, we have an IWorkItem interface that’s implemented by several concrete classes.  We also have a decorator called RetryableWorkItem that wraps any IWorkItem instance and watches for it to throw exceptions when it’s executed.  If it does so, then RetryableWorkItem will inspect the thrown exception to see if it’s worth retrying, and if so, it will throw a RetryableException that tells our scheduler that the work item blew up and needs to be executed again sometime in the future.

[TestClass]

public class when_a_retryable_error_occurs : RetryableWorkItemContext

{

    private Exception exceptionToThrow;

    private Exception thrownException;

    protected override void Context()

    {

        base.Context();

        this.exceptionToThrow = new IOException();

        this.actualWorkItem.Stub(x => x.Execute()).Throw(this.exceptionToThrow);

    }

    protected override void BecauseOf()

    {

        this.thrownException = ((MethodThatThrows)delegate

        {

            this.retryableWorkItem.Execute();

        }).GetException();

    }

    [TestMethod]

    public void should_tell_the_scheduler_to_retry_the_work_item()

    {

        this.thrownException.Is(typeof(RetryableException));

    }

    [TestMethod]

    public void should_preserve_the_original_error()

    {

        this.thrownException.InnerException.ShouldBeTheSameAs(this.exceptionToThrow);

    }

    [TestMethod]

    public void should_provide_a_retry_delay()

    {

        RetryableException retryEx = this.thrownException as RetryableException;

        retryEx.TimeToWaitBeforeRetry.ShouldBeGreaterThan(TimeSpan.Zero);

    }

}

In the BecauseOf() method, we use a spec extension on the delegate type that will execute the delegate, catch any exception that is thrown, and return the exception.  We just save it in a class field and then inspect it in the tests.

Here’s the spec extension that captures the exception for us:

///

/// Returns an exception thrown by a delegate, or asserts if no exception was thrown.

///

/// The delegate to be executed.

/// The exception that was thrown.

public static Exception GetException(this MethodThatThrows testCode)

{

    try

    {

        testCode();

    }

    catch (Exception e)

    {

        return e;

    }

    Assert.Fail(“The delegate did not throw an exception.”);

    return null;

}

 

Edit: one nice side benefit of this approach is that you guarentee that the action you’re testing is the one that threw the exception.  With the stock MSTest exception attribute, all that’s really tested is that something, anything, in your test threw the expected exception.  If you’re testing for a common exception like InvalidOperationException, you might accidentally have that exception thrown by some supporting object and while the test passes, it’s not testing what you think you’re testing.  This approach avoids that problem. Another benefit is that there might be certain behaviors that you expect to see even when an exception is thrown.  For example, maybe if you pass bad parameters into a service method you expect to get an ArgumentException but first you want the bad call to be written to the event log.  The MSTest ExpectedException attribute doesn’t work for that either because it gives you no opportunity to verify stuff after the exception is thrown.  This approach does.

 

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.