Yet another BDD extension for xUnit
I’ve been “test infected” for a while and while I still lack the hardcore discipline to apply TDD to everything I do, I have managed to use it through a couple of projects I have worked on. One of the things I noticed as I was writing more and more tests is that I went from testing a scenario per method (with lots of test methods per class) to testing a scenario per class. Looking around the web, I think a lot of people go through this evolution toward BDD style testing where you set up a context, apply some action to it and then veryify the results are what you expected.
For my next project, I wanted to formalise this style of testing and make it as easy as possible to do, to that end I did a bunch of research, selected xUnit as my testing framework and then stole incorporated ideas from all over the place to come up with something that allowed me to write tests in the following style:
public class When_popping_an_empty_operand_stack
{
private OperandStack _operandStack;
private long _value;
public Action Given()
{
return () => { _operandStack = new OperandStack(); };
}
public Action When()
{
return () => _value = _operandStack.Pop();
}
[Then]
public void the_value_should_be_zero()
{
Assert.Equal( 0L, _value );
}
}
Note: This is where I ended up after numerous revisions determining what I could and could not do while trying to keep the noise and ceremony to a minimum.
This approach treats the test class as the context (or world) that you set up using the Given delegate, alter using the When delegate and verify the outcomes in the methods marked with the [Then] attribute. If you’ve seen MSpec then you are probably thinking “Hey, thats just like MSpec… but not quite” and you’d be dead right. Of all the approaches and syntaxes I saw, MSpec had the best and it was the one that came the closest to doing what I wanted, apart from one (not so minor) issue – the R# test runner does not play nice with MSpec.
To achieve the above test using xUnit was relatively simple and borrowed heavily from this posting by Frederik Kalseth. The reason I didn’t simply use his version was the requirement to inherit from a common base class.
A final hat tip must be given to Phil Haack as it was his post on Streamlined BDD Using SubSpec for xUnit.NET that set me down the path of playing with xUnit to make it test the way I wanted to.
Going forward, I’ve been looking at xUnits IUseFixture interface and wondering about using Fixtures as a means for encapsulating context where you want to share it across multiple test classes… but I’ll solve that when I get around to needing it. Code for making xUnit behave as shown in the code above is below. Feedback on both the test approach (Given / When / Then) and the implementation of the xUnit extensions is most welcome.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class ThenAttribute : FactAttribute
{
protected override IEnumerable<ITestCommand> EnumerateTestCommands(MethodInfo method)
{
foreach( var testCommand in base.EnumerateTestCommands( method ) )
{
yield return new ThenCommand( testCommand );
}
}
}
public class ThenCommand : ITestCommand
{
private readonly ITestCommand _innerCommand;
public ThenCommand( ITestCommand innerCommand )
{
_innerCommand = innerCommand;
}
public MethodResult Execute( object testClass )
{
var given = GetGivenAction( testClass );
EnsureGivenIsValid( given );
var when = GetWhenAction( testClass );
EnsureWhenIsValid( when );
given();
when();
return _innerCommand.Execute( testClass );
}
public XmlNode ToStartXml()
{
return _innerCommand.ToStartXml();
}
public string DisplayName
{
get { return _innerCommand.DisplayName; }
}
public bool ShouldCreateInstance
{
get { return _innerCommand.ShouldCreateInstance; }
}
private Action GetGivenAction( object testClass )
{
return GetAction( testClass, "Given" );
}
private void EnsureGivenIsValid( Action given )
{
if (given == null)
{
throw new ArgumentException( "Test class does not contain a Given() method "
+ "or the Given() method does not return an Action delegate. "
+ "Please ensure the test class implements a public method with the following signature: "
+ "Action Given()");
}
}
private Action GetWhenAction( object testClass )
{
return GetAction( testClass, "When" );
}
private void EnsureWhenIsValid(Action when)
{
if (when == null)
{
throw new ArgumentException("Test class does not contain a When() method "
+ "or the When() method does not return an Action delegate. "
+ "Please ensure the test class implements a public method with the following signature: "
+ "Action When()");
}
}
private Action GetAction(object testClass, string methodName)
{
var wrappedTestClass = Reflector.Wrap(testClass.GetType());
var givenMethod = wrappedTestClass.GetMethod( methodName );
if (givenMethod == null)
{
return null;
}
if(givenMethod.ReturnType != typeof(Action).FullName)
{
return null;
}
return (Action)givenMethod.MethodInfo.Invoke( testClass, null );
}
}