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.
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:
[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:
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:
- Download the pre-compiled beta assembly from CodePlex and add a reference to it from your test project.
- 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
. - In the class'
Run
override, call GivenThat<TInitializer>()
, When<TAction>()
,
and Then<TAssertion>()
, specifying English-language sentences (in Pascal case) for the type arguments. - 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:
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:
[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:
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
:
.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:
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:
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:
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:
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:
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.
[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:
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.
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.
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.
public interface IAction<TTarget> ...
public interface IAction<TTarget, TReturnType> ...
Both action interfaces have a single method, with a different signature.
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.
public interface IAssertion<TTarget> ...
public interface IAssertion<TTarget, TReturnType> ...
There is one method in the interfaces, Verify
:
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.
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.
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:
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 Initializer
s 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.