Behavioral: A BDD library for better organizing your unit tests
- Download source - 42.89 KB
- Download binaries - 4.25 KB
- Behavioral can be Download latest files from codeplex site.
Introduction
Behavioral is a .NET assembly, written in C#, that can be used in conjunction with your usual testing framework (eg: NUnit, MSTest) to add a more BDD-like syntax to your unit tests. It is currently in Alpha 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.
Using the Code
Quickstart
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 alpha assembly from codeplex and add a reference to it from your test project.
- Create a new class deriving from
Behavioral.UnitTest<TTarget>
orBehavioral.UnitTest<TTarget, TReturnType>
. The latter is required for testing methods that return a type, ie: are notvoid
. - In the class' constructor, call
GivenThat<TInitializer>()
,When<TAction>()
andThen<TAssertion>()
, specifying English-language sentences (in Pascal Case) for the type arguments. - Define the classes in part 3, implementing
IInitializer<TTarget>
,IAction<TTarget>
andIAssertion<TTarget>
, 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 interface that must be used for defining actions and assertions.
The
UnitTest
classes have a Run
method that should be used for specifying the test:
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 matches the assertion specification. This also returns a fluent interface, much like
GivenThat
:
Then<ResultShouldEqual>(58)
.And<SomeOtherPostCondition>()
.And<FurtherPostCondition>();
Notice that parameters have been supplied for the
When
and Then
. This is also possible for
GivenThat
calls. Any parameters 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
There are two initializers that are available,
IInitializer<TTarget>
and its specialization IInitializerWithTearDown<TTarget>
. Both contain a
SetUp
method which accepts a reference of the target type. However, the latter also contains a
TearDown
method which is called after the action has been executed.
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.
All initializers are called as soon as
GivenThat
is called from the UnitTest
.
Actions
There are two actions 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
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();
}
}
History
07/28/2011 Alpha version 0.9.0.0 released.
Download
Either download from CodeProject:
Download Behavioral 0.9.0.0 (zip) 4.25 KB
Download Behavioral 0.9.0.0 Alpha Source (zip) - 42.89 KB
Or from its CodePlex site.