Its been a while since I evaluated and evolved my approach to BDD. The way I’ve been doing BDD up until now is described in this blog post which goes way back to 2008. Everyone has kind of their own style nowadays. Below you can find the code of two context/specifications for a method named MakePreferred on a Customer class. This simple example clarifies the style that I’ve been following up until now.
The bottom line of this example is that preferred customers get a 10 percent discount. Customers that are already preferred do not get an additional discount or otherwise we’re out of business ;-).
I’ve been pretty happy with this approach so far, although sometimes there were some quirks associated with this. So it was time for me to look beyond the horizon again, trying to look for ways to improve.
Machine.Specifications or MSpec for short is something that has been on my ‘cool-things-to-learn-list’ for quite some time now. As you will see later in this post, the syntax is a bit different as one would come to expect from a context/specification framework that targets the C# programming language. Its seems to be heavily inspired by Scott Bellware’s SpecUnit framework and RSpec.
Lets see how to set things up first.
The most obvious starting point is downloading the bits and bytes. You can grab the source code from GitHub and build it or you can wuss out like I did and get the latest build from the TeamCity.CodeBetter.com builder server (you can log on as a guest and search the artifacts for a latest build).
When you’re heavily addicted to TestDriven.NET like I am, then its possible to keep using this wonderful Visual Studio add-in for running MSpec context/specifications. Just create a directory named Machine.Specifications in {$Program_Files}\TestDriven.NET 2.0 and copy the following files:
- Machine.Specifications.dll
- Machine.Specifications.TDNetRunner.dll
- InstallTDNetRunner.bat
- Run the InstallTDNetRunner.bat file and you’re able to run all MSpec context/specifications using TestDriven.NET.
I also strongly encourage you to install the plugin for the Resharper test runner (if only to prevent some Resharper warnings later on). First step is to add a directory named Plugins to the Bin directory of Resharper ({$Program_Files}\JetBrains\ReSharper\v4.5\Bin\). Then create a directory named Machine.Specifications in the Plugins directory you just created and copy the following files:
- Machine.Specifications.dll
- Machine.Specifications.ReSharperRunner.4.5.dll
- InstallResharperRunner.4.5.bat
Run the InstallResharperRunner.4.5.bat file and you’re also able to run MSpec context/specifications using the Resharper test runner.
I’m not going to put this off any longer. Lets look at the code of the context/specifications shown earlier but completely revamped using the MSpec syntax:
[TestFixture]
public class When_making_a_regular_customer_preferred
: ContextSpecification
{
protected override Customer Create_subject_under_test()
{
return new Customer(new[] { _order });
}
protected override void Establish_context()
{
_order = new Order(new[] { new OrderItem(12),
new OrderItem(16) });
_totalAmountWithoutDiscount = _order.TotalAmount;
}
protected override void Because()
{
SUT.MakePreferred();
}
[Test]
public void Then_the_customer_should_be_marked_as_preferred()
{
SUT.IsPreferred.ShouldBeTrue();
}
[Test]
public void Then_a_ten_percent_discount_should_be_applied_to_all_outstanding_orders()
{
_order.TotalAmount.ShouldBeEqualTo(
_totalAmountWithoutDiscount * 0.9);
}
private Order _order;
private Double _totalAmountWithoutDiscount;
}
[TestFixture]
public class When_making_a_preferred_customer_preferred
: ContextSpecification
{
protected override Customer Create_subject_under_test()
{
var customer = new Customer(new[] { _order });
customer.MakePreferred();
return customer;
}
protected override void Establish_context()
{
_order = new Order(new[] { new OrderItem(12),
new OrderItem(16) });
_totalAmountWithoutDiscount = _order.TotalAmount;
}
protected override void Because()
{
SUT.MakePreferred();
}
[Test]
public void Then_no_additional_discount_should_be_applied_to_the_outstanding_orders()
{
_order.TotalAmount.ShouldNotBeEqualTo(
_totalAmountWithoutDiscount * 0.81);
}
private Order _order;
private Double _totalAmountWithoutDiscount;
}
//
// Subject under test
//
public class Customer
{
private readonly List _orders;
public Boolean IsPreferred { get; private set; }
public Customer(IEnumerable orders)
{
_orders = new List(orders);
}
public void MakePreferred()
{
if(IsPreferred)
return;
IsPreferred = true;
_orders.ForEach(order => order.ApplyDiscount(10));
}
}
I warned you about the syntax, didn’t I :-). It only took me a couple of seconds to get used to this syntax but now I’m completely hooked. Instead of using methods and attributes, MSpec utilizes delegates and anonymous methods. But there’s more.
When using NUnit for writing context/ specifications, the Establish_context and Because methods of the example shown earlier is executed before every observation (test). With MSpec, the Establish and Because anonymous methods are executed only once for every context no matter how many observations a particular context class contains. Big difference? Well, at first glance not but on second hand it does make selling out on the context a bit more difficult as it will probably blow up in your face sooner than later. You can force MSpec to execute the Establish and Because anonymous methods before every observation by applying the SetupForEachSpecification attribute to the context class, but I strongly encourage you to stay away from that unless absolutely needed.
Also notice that the fields in the contexts are now all static. This is needed so that the anonymous methods can access those.
Running these context/specifications using TestDriven.NET yields the following output in the output window of Visual Studio:
Making a customer preferred, when a regular customer is made preferred
» should mark the customer as preferred
» should apply a ten percent discount to all outstanding ordersMaking a customer preferred, when a preferred customer is made preferred
» should apply no additional discount to the outstanding orders
What’s not to like? Well, the only downside so far is that Resharper was giving me some warnings about classes and fields not being used etc. … . Many of those warnings disappeared by registering the MSpec plugin for the Resharper test runner as I explained earlier.
So far, so good. I’ve got two more posts coming up on MSpec, so stay tuned.