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:

  1. Download the pre-compiled alpha 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, ie: are not void.
  3. In the class' constructor, 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<TTarget>, IAction<TTarget> and IAssertion<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.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架