Adam Ralph

Software, tea, and snowboarding

Alternate Code for dotnetConf Test Driving .NET

Keith Burnell gave a great talk today titled Test Driving .NET at dotnetConf.

For anyone who might be interested in using alternate frameworks for TDD in .NET, I thought I’d reproduce the code shown during his talk using a couple of my own OSS projects, FakeItEasy and xBehave.net. I’ve taken the version of the code before the IoC was introduced in the last part of the talk because this is not affected by the changes which I’d like to demonstrate.

The original code from Keith’s talk (using NUnit, RhinoMocks and FluentAssertions) looked something like this (using var where possible):

[Test]
public void Add_ShouldReturn_12_When_Passed_8_And_4()
{
	//Arrange
	const decimal input1 = 8;
	const decimal input2 = 4;
	var mockRepository = new MockRepository();
	var validationServiceMock = mockRepository
		.StrictMock<IValidationService>();
	validationServiceMock
		.Expect(x => x.ValidateForAdd(input1, input2))
 			.Return(True).Repeat.Once;
	var classUnderTest =
		new CalculatorService(validationServiceMock);
	//Act
	var result = classUnderTest.Add(input1, input2);
	//Assert
	mockRepository.VerifyAll();
	result.Should().Be(12);
}

Here is the test re-written using FakeItEasy instead of RhinoMocks:

[Test]
public void Add_ShouldReturn_12_When_Passed_8_And_4()
{
	//Arrange
	const decimal input1 = 8;
	const decimal input2 = 4;
	var validationService = A.Fake<IValidationService>();
	A.CallTo(() => validationService
 				.ValidateForAdd(input1, input2))
 			.Returns(true);
	var classUnderTest = new CalculatorService(validationService);
	//Act
	var result = classUnderTest.Add(input1, input2);
	//Assert
	A.CallTo(() => validationService
 				.ValidateForAdd(input1, input2))
 			.MustHaveHappened(Repeated.Exactly.Once);
	result.Should().Be(12);
}

FakeItEasy requires less ceremony than RhinoMocks (no MockRepository), makes no distinction between stubs/mocks and has a fluent, easy to read DSL (a call to X returns Y).

And here is the test (now a scenario) re-written using both FakeItEasy and xBehave.net:

[Scenario]
public void Addition(
	decimal input1,
	decimal input2,
	IValidationService validationService,
	CalculatorService classUnderTest,
	decimal result)
{
	"Given an input of 8"
		.Given(() => input1 = 8);

	"And an input of 4"
		.And(() => input2 = 4);

	"And a validation service"
		.And(() =>
		{
			validationService = A.Fake<IValidationService>();
			A.CallTo(() => validationService
 						.ValidateForAdd(input1, input2))
 					.Returns(true);
		});

	"And a calculation service"
		.And(() => classUnderTest =
 				new CalculatorService(validationService);
	
	"When I add the inputs"
		.When(() => result = classUnderTest.Add(input1, input2);
	
	"Then the input must have been validated"
		.Then(() => A.CallTo(() =>
 					validationService.ValidateForAdd(input1, input2))
			.MustHaveHappened(Repeated.Exactly.Once));
 		
	"And the result should be 12"
		.And(() => result.Should().Be(12));
}

As ever, the benefits of using xBehave.net are primarily readability (read the English to understand what the test is doing and only dive into the implementation if you need to) and a test per step (if one step fails the test output tells you exactly which one failed rather than only which test method).

It’s also easy to add further examples when using xBehave.net with the Example attribute:

[Scenario]
[Example(8, 4, 12)]
[Example(7, 9, 16)]
[Example(100, 200, 300)]
public void Addition(
	decimal input1,
	decimal input2,
	decimal expectedResult,
	IValidationService validationService,
	CalculatorService classUnderTest,
	decimal result)
{
	"Given an input of {0}"
		.Given(() => { });

	"And an input of {1}"
		.And(() => { });

	"And a validation service"
		.And(() =>
		{
			validationService = A.Fake<IValidationService>();
			A.CallTo(() => validationService
 						.ValidateForAdd(input1, input2))
 					.Returns(true);
		});

	"And a calculation service"
		.And(() => classUnderTest =
 				new CalculatorService(validationService);
	
	"When I add the inputs"
		.When(() => result = classUnderTest.Add(input1, input2);
	
	"Then the input must have been validated"
		.Then(() => A.CallTo(() =>
 					validationService.ValidateForAdd(input1, input2))
 				.MustHaveHappened());
 		
	"And the result should be {2}"
		.And(() => result.Should().Be(expectedResult));
}