Click here to Skip to main content
15,886,771 members
Articles / Programming Languages / C#

Behavioral: A BDD library for better organizing your unit tests

Rate me:
Please Sign up or sign in to vote.
4.89/5 (6 votes)
24 Aug 2011BSD9 min read 27.4K   250   28   7
A Behavior Driven Design (BDD) library adding a clearer syntax and better organization to unit tests. Currently tested working with NUnit and MSTest.

Introduction

Behavioral is a .NET assembly, written in C#, that can be used in conjunction with your usual testing framework (e.g.: NUnit, MSTest) to add a more BDD-like syntax to your unit tests. It is currently in Beta phase, so your feedback can make Behavioral better.

BehavioralActivity.png

Background

Behavior Driven Development (BDD) is the natural next step after Test Driven Development (TDD). BDD is many different things, but one aspect of BDD is addressed by Behavioral: unit test organization.

The usual way of organizing unit tests is as follows:

C#
[TestFixture]
public class CalculatorFixture
{
    [SetUp]
    public void SetUp()
    {
        this.calculator = new Calculator();
    }

    [Test]
    public void CanAddTwoPositiveNumbers()
    {
        int result = this.calculator.Add(13, 45);
        Assert.AreEqual(58, result);
    }

    [Test]
    public void AdditionOverflowCausesException()
    {
        Assert.Throws<OverflowException>(() => 
                      this.calculator.Add(int.MaxValue, 1));
    }

    private Calculator calculator;
}

As the tests become more complex and involved, there are two problems with this approach that are addressed by Behavioral.

  • The tests do not promote reuse, neither of the initialization code, the action under test, nor the assertions that are made after the fact.
  • The tests can become hard to understand and, as the tests in an agile project form a reliable documentation of the code's intent, it is important to keep them simple.

With Behavioral, the two tests above become this:

C#
using Behavioral;
using NUnit;

namespace MyTests
{
    [TestFixture]
    public class AddingTwoPositiveNumbersShouldGiveCorrectResult : 
                 UnitTest<Calculator, int>
    {
        [Test]
        public override void Run()
        {
            GivenThat<CalculatorIsDefaultConstructed>()

            .When<TwoNumbersAreAdded>(13, 45)

            .Then<ResultShouldMatch>(58);
        }
    } 

    [TestFixture]
    public class AddingNumbersThatCausesOverflowShouldThrowOverflowException : 
                 UnitTest<Calculator, int>
    {
        [Test]
        public override void Run()
        {
            GivenThat<CalculatorIsDefaultConstructed>()

            .When<TwoNumbersAreAdded>(int.MaxValue, 1)

            .ThenThrow<OverflowException>();
        }
    }
}

This is much more readable to anyone who wishes to discern the intent of the code from the tests or perform maintenance on the tests. Also, the tests reuse code which can cut down on test errors and speed up the test-first approach.

Change List

Beta (0.9.9.5)
Fixed bug with clearing context at start of run. Apologies.
Beta (0.9.9.4)
Cleared context at start of run (deals with stale context when not threaded).
Fixed probable bug in exceptions being swallowed when not using ThenThrow.
Made Then and ThenThrow mutually exclusive
Moved to a fully Fluent interface [GivenThat().And().When().Then().And().And().ThenThrow()]
Fixed threading bug in context.
Added more initializer collection stuff.
Started adding initializer collections.
Allowed actions to be run as initializers.
Allowed context-free initializers.
Removed IInitializeWithTearDown and replaced with ITearDown.
Added IErrorHandler.
Allowed initializers to reference anonymous context state.
Context can now be named, allowing multiple values of the same type.

Using the Code

Quick Start

If you just want to dive in to Behavioral quickly, then here a few steps to help you on your way:

  1. Download the pre-compiled beta assembly from CodePlex and add a reference to it from your test project.
  2. Create a new class deriving from Behavioral.UnitTest<TTarget> or Behavioral.UnitTest<TTarget, TReturnType>. The latter is required for testing methods that return a type, i.e.: are not void.
  3. In the class' Run override, call GivenThat<TInitializer>(), When<TAction>(), and Then<TAssertion>(), specifying English-language sentences (in Pascal case) for the type arguments.
  4. Define the classes in part 3, implementing IInitializer, IAction, and IAssertion, respectively.

How to Use Behavioral

Unit Test

The starting point for all unit tests is inheriting from one of the UnitTest abstract base classes:

C#
public abstract class UnitTest<TTarget> ...
public abstract class UnitTest<TTarget, int> ...

The former class is for use with methods that do not have a return value, while the latter requires the method under test to return the specified type.

Which class you choose has an impact on the interfaces that can be used for defining actions and assertions.

The UnitTest classes have a Run method that should be used for specifying the test:

C#
[TestMethod]
public override void Run()
{
    GivenThat<CalculatorIsDefaultConstructed>()
    .When<AddingTwoNumbers>(13, 45)
    .Then<ResultShouldEqual>(58);
}

The GivenThat method requires a type argument which implements an IInitializer interface. GivenThat returns a Fluent interface, allowing you to chain preconditions together:

C#
GivenThat<CalculatorIsDefaultConstructed>()
        .And<SomeOtherInitializationCode>()
        .And<FurtherInitializationCode>()

The When method requires a type argument which matches the action specification. However, you can also pass in either an Action<TTarget> or a Func<TTarget, TReturnType>. Note that this will circumvent the English-language of the type argument style, but some actions are too simple to necessitate a new class definition.

The Then method requires a type argument which implements the IAssertion interface. This also returns a Fluent interface, much like GivenThat:

C#
.Then<ResultShouldEqual>(58)
.And<SomeOtherPostCondition>()
.And<FurtherPostCondition>();

Notice that parameters have been supplied for When and Then. This is also possible for GivenThat calls. Any parameter can be passed in here as the methods take params object[] as their argument. These values will be passed to the constructor of the supplied type. Notice, however, that any type mismatches will not be caught at compile time. In fact, failing to supply the correct number of arguments will not be caught at compile time.

Initializers

Initializers are the building blocks of unit tests. The whole point of Behavioral is to encourage the reuse and composition of initializing code so that it forms a readable script for setting up a test. How granular your initializers are is entirely up to you, but they can be logically grouped using InitializerCollections (see below).

The standard initializer looks like this:

C#
public class CalculatorIsDefaultConstructed : IInitializer<Calculator>
{
    public void SetUp(ref Calculator calculator)
    {
        calculator = new Calculator();
    }
}

Notice that, because the argument is passed by reference, we can not only mutate its properties, we can also alter the reference itself. Furthermore, the type argument supplied here matches the overall type of the unit test in which the initializer will be used. However, this is not always useful because some initializers do not rely on any context whatsoever. Take this real-world example:

C#
class UnityContainerIsMocked : IInitializer<SecurityCommandsUser>
{
    public void SetUp(ref SecurityCommandsUser userCommands)
    {
        var unityContainer = Isolate.Fake.Instance<IUnityContainer>();
        this.SetContext(unityContainer);
    }
}

In this case, we are mocking the Unity container - a common practice in the Prism application that I am currently working on. The problem here is obvious - we have tied ourselves to the SecurityCommandsUser class, but the set-up method completely ignores it. In the alpha release of Behavioral, this precluded reuse of such initializers (the same initializer would have to be rewritten for SecurityCommandsApplication, for example). In the beta, we do not have to tie our initializers to the targeted type of the unit test:

C#
class UnityContainerIsMocked : IInitializer
{
    public void SetUp()
    {
        var unityContainer = Isolate.Fake.Instance<IUnityContainer>();
        this.SetContext(unityContainer);
    }
}

Much better. Now, for a little technical diversion... The way that this works is slightly dirty, but necessary, and yields a potential problem that was turned into a feature. In .NET, generic constraints are not part of the method signature. This is by design and entirely expected behavior on behalf of the compiler. However, it's a bit of an inconvenience when you want to do something like this:

C#
IInitializationFluent<TTarget> GivenThat<TInitializer>(params object[] constructorArgs)
        where TInitializer : IInitializer<TTarget>;

IInitializationFluent<TTarget> GivenThat<TInitializer>(params object[] constructorArgs)
        where TInitializer : IInitializer;

This isn't valid because the runtime isn't able to distinguish between these two methods - they are identical in its eyes. This means that we have to work around the problem, in a slightly dirty way. Long story short, any interface that can be used as an initializer is now given the marker interface IInitializerMarker. At run time, there's then some type-sniffing to discover exactly what we're dealing with and how to run it. This breaks the Open/Closed principle, which is unfortunate, but it gives us the chance to add some more features. Firstly, we can support context-free initializers, as well as initializers that operate on the target type. However, what would have been a compile-time check - if the code above worked - is instead a runtime check. This means that you can use any TTarget value in IInitializer<TTarget> in any UnitTest. Hmm, we've opened it up a bit too far... To make sense of this, we can leverage the Context of the test. If we assume that the target type of the test is ISession (i.e.: you are testing NHibernate mappings or some such), this initializer is perfectly valid:

C#
public class TheUserIsMocked : IInitializer<User>
{
    public void SetUp(ref User user)
    {
        user = Isolate.Fake.Instance<User>();
    }
}

In this example, the User reference comes from the current unit test's Context. So, there is a valid use for an IInitializer with a target type that does not match the UnitTest's target type, meaning that the lack of compile-time check is moot, and we get some nice extra functionality. It's also worth noting that Actions can be used as initializers, but the reverse is not valid. When an action is used as an intiailizer, any return value is discarded.

C#
[TestClass]
public class AddingNumbersThatCauseOverflowShouldThrowOverflowException : 
       UnitTest<Calculator, int>
{
    [TestMethod]
    public override void Run()
    {
        GivenThat<CalculatorIsReadyToRun>()
            .And<AddingTwoNumbers>(23, 32)

        .When<AddingTwoNumbers>(int.MaxValue, 1)

        .ThenThrow<OverflowException>();
    }
}

Initializer Collections

Even with a Fluent interface, it can become laborious to retype the same few initializers every time you create a unit test. For that reason, initializers can be grouped into collections so that a baseline for every test can be composed of more discrete parts:

C#
public class CalculatorIsReadyToRun : InitializerCollection
{
    protected override void Register()
    {
        GivenThat<CalculatorIsDefaultConstructed>()
            .And<SomeOtherInitialization>()
            .And<MoreInitialization>()
            .And<EvenMoreIntialization>()
            .And<ThankGodForInitializerCollections>();
    }
}
...
[TestClass]
public class AddingNumbersThatCauseOverflowShouldThrowOverflowException : 
             UnitTest<Calculator, int>
{
    [TestMethod]
    public override void Run()
    {
            GivenThat<CalculatorIsReadyToRun>()
                    .And<TestSpecificInitializer>()

            .When<AddingTwoNumbers>(int.MaxValue, 1)

            .ThenThrow<OverflowException>();
    }
}

ITearDown

In Behavioral alpha, tear down was coupled with the IInitializer interface to form IInitializerWithTearDown. This lapse broke the the Interface Segregation principle and has been reversed. I would have marked that interface as obsolete, but it was an alpha release with limited downloads, so I'm afraid I just removed it entirely. So, this is one of many breaking changes between the alpha and beta releases. The point of a tear down is to perform some deinitialization after the unit test's action has been called.

C#
public class SessionHasBeenStarted : IInitializer<ISession>, ITearDown<ISession>
{
    public void SetUp(ref ISession session)
    {
        this.SetContext(session.BeginTransaction());
    }

    public void TearDown(ISession session)
    {
        var transaction = this.GetContext<ITransaction>();
        if(transaction != null)
        {
            transaction.Commit();
        }
        if(session != null)
        {
            session.Dispose();
        }
    }
}

IErrorHandler

But, what if there is an error in the midst of the action call? After all, some unit tests are intended to make the action throw an exception, for example. Well, the answer is the IErrorHandler interface. If an exception is thrown by the test, then all IErrorHandler interfaces registered as part of IInitializerMarker implementations will be invoked.

C#
public class SessionHasBeenStarted : IInitializer<ISession>, 
            ITearDown<ISession>, IErrorHandler<ISession>
{
    public void SetUp(ref ISession session)
    {
        this.SetContext(session.BeginTransaction());
    }

    public void TearDown(ISession session)
    {
        var transaction = this.GetContext<ITransaction>();
        if(transaction != null)
        {
            transaction.Commit();
        }
        if(session != null)
        {
            session.Dispose();
        }
    }

    public void OnError(ISession session)
    {
        var transaction = this.GetContext<ITransaction>();
        if(transaction != null)
        {
            transaction.Rollback();
        }
        if(session != null)
        {
            session.Dispose();
        }
    }
}

It is worth noting that ITearDown and IErrorHandler on their own are not sufficient to register them with a Unit Test - they must be coupled with IInitializer. This is to ensure that setup, tear down, and error handling remain symmetric.

Actions

There are two action interfaces, and the one that you choose is dictated by the UnitTest class that was inherited from.

C#
public interface IAction<TTarget> ...
public interface IAction<TTarget, TReturnType> ...

Both action interfaces have a single method, with a different signature.

C#
void Execute(TTarget target);
TReturnType Execute(TTarget target);

The Execute method will be called as soon as the When method is called from the UnitTest subclass.

Assertions

Again, there are two assertion interfaces which must match the UnitTest base.

C#
public interface IAssertion<TTarget> ...
public interface IAssertion<TTarget, TReturnType> ...

There is one method in the interfaces, Verify:

C#
void Verify(TTarget target);

void Verify(TTarget target, TReturnType returnValue);

In the implementation of these methods, you should use your unit test framework's Assert methods to verify that the test has passed.

Exceptions

Sometimes the expected behavior of a method is to throw an exception. In Behavioral, this can be achieved by calling ThenThrow<TException> in the UnitTest.Run method instead of making any Then calls.

C#
ThenThrow<OverflowException>();

Context

Sometimes, further context is required throughout a unit test, above and beyond the supplied target class and the target method's return value.

Inside the initializers, you can call the SetContext<TContext>(TContext contextValue) method for use inside the action or assertion classes.

C#
public class SessionIsStarted : IInitializerWithTearDown<ISession, int>
{
    public void SetUp(ref ISession session)
    {
        session = SessionFactory.CreateSession();
        SetContext<ITransaction>(session.BeginTransaction());
    }

    public void TearDown(ISession session)
    {
        GetContext<ITransaction>().Commit();
        session.Clear();
        session.Dispose();
    }
}

From the beta release, you can now name the context, so that you can have more than one value of the same type:

C#
public class CalculatorIsDefaultConstructed : IInitializer<Calculator>
{
    public void SetUp(ref Calculator calculator)
    {
        calculator = new Calculator();

        this.SetContext(int.MaxValue);
        this.SetContext("one", 1);
        this.SetContext("two", 2);
        this.SetContext("three", 3);
    }
}

Internally, two separate collections are maintained: one for anonymous context (which int.MaxValue will be saved into, associated with the System.Int32 type) and another for named context. When using Initializers which leverage the context, the anonymous collection is always used.

Footnote

You may have noticed that you have lost the ability to perform a baseline setup/teardown for a group of tests. This is a common requirement when, for example, testing against a database or some other externality that incurs a large initialization cost. Because each test is its own class, rather than packaging a test per method as is usual, NUnit's [FixtureSetup] and MSTest's [ClassInitialize] become moot.

Thankfully, there are alternatives. In NUnit, you can use the [SetupFixture] attribute, which operates on a namespace level. MSTest's [AssemblyInitialize] is similar, but operates on a per-assembly basis.

History

  • 07/28/2011: Alpha version 0.9.0.0 released.
  • 08/23/2011: Beta version 0.9.9.4 released.
  • 08/24/2011: Beta version 0.9.9.5 released.

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
Software Developer (Senior) Nephila Capital Ltd.
Bermuda Bermuda
An experienced .NET developer, currently working for Nephila Capital Ltd. in Bermuda. Author of "Pro WPF and Silverlight MVVM".

Comments and Discussions

 
QuestionFluent Like StoryQ Pin
jboarman1-Aug-11 12:27
jboarman1-Aug-11 12:27 
AnswerRe: Fluent Like StoryQ [modified] Pin
garymcleanhall1-Aug-11 17:14
garymcleanhall1-Aug-11 17:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.