Click here to Skip to main content
15,880,427 members
Articles / Programming Languages / C#

Towards Better Unit Testing Organization…

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
27 Jul 2011CPOL4 min read 16.4K   3   5
Towards better unit testing organization

I try to follow a test-first approach at all times. However, the way in which I organize my unit tests has evolved over the past five years or so.

Originally, I would follow the pattern espoused in unit testing tutorials:

C#
[TestFixture]
public class CalculatorFixture
{
	[Test]
	public void CanAddTwoPositiveNumbers()
	{
		var calculator = new Calculator();
		int result = calculator.Add(13, 45);
		Assert.AreEqual(58, result);
	}

	[ExpectedException(typeof(OverflowException))]
	[Test]
	public void OverflowCausesException()
	{
		var calculator = new Calculator();
		calculator.Add(int.MaxValue, 1);
	}
}

This is pretty much as simple as it gets. However, it is time-consuming and does not promote reuse of repeated code. While we can move initialization into methods marked [SetUp], this will be run for every test in the fixture, which might not be suitable.

So, I factored things out into a proper Arrange, Act, Assert pattern. I forget where I picked this up from, but – as with many things – I spotted it, briefly evaluated it and decided to run with it.

C#
[TestFixture]
public abstract class TestBase
{
	[SetUp]
	public void Init()
	{
		GivenThat();
		When();
	}

	protected virtual void GivenThat()
	{
	}

	protected abstract void When();
}

This is still fairly simple, but much more expressive. It allows me to specify my tests as follows:

C#
[TestFixture]
public abstract class WhenUsingTheCalculator : TestBase
{
	protected abstract override void GivenThat()
	{
		base.GivenThat();
		this.calculator = new Calculator();
	}

	private Calculator calculator;
}

public abstract class WhenAddingTwoNumbers : WhenUsingTheCalculator
{
	protected override void When()
	{
		this.result = this.calculator.Add(X, Y);
	}

	protected abstract int X { get; }

	protected abstract int Y { get; }

	protected int result;
}

public class WhenAddingTwoPositiveNumbers : WhenAddingTwoNumbers
{
	protected override int X { get { return 13; } }

	protected override int Y { get { return 45; } }

	[Test]
	public void ItShouldReturnTheCorrectResult()
	{
		Assert.AreEqual(58, this.result);
	}
}

public class WhenAddingNumbersThatCauseOverflow : WhenAddingTwoNumbers
{
	protected override int X { get { return int.MaxValue; } }

	protected override int Y { get { return 1; } }

	[Test]
	public void ItShouldThrowAnOverflowException()
	{
	}
}

This style isn’t much of an improvement, to be entirely honest. Sure, it generates a nice output when a test fails (“WhenAddingNumbersThatCauseOverflow.ItShouldThrowAnOverflowException() failed”). Note that the latter test won’t work, though. More work is required to allow expected exceptions to be caught (Hint: The exception in thrown in When() and the [Test] methods are run after this).

I persevered with an enhanced version of this for a while though, because the reuse seemed worth the pain of such a leaky abstraction. But, the fact that the reuse was through inheritance ended up causing a bit of a problem, because anyone who came to the tests after they were originally written were flummoxed by the complex, deep hierarchies. Sometimes this person was me…

I recently decided to go back to the drawing board to try to find something that would hit the following requirements:

  • Support the reuse of initialization code and assertions
  • Rely more on composition than inheritance for creating tests
  • Be really easy to understand for anyone seeing tests for the first time (tests are a reliable form of documentation)

The result is as follows:

Unit Testing domain

First, a quick explanation. A unit test has very simple behavior: Arrange() the preconditions, Act() on the target object, Assert() the postconditions. This is probably all a test runner will need to know about a unit test (if that, it could probably just ‘Run()‘ unit tests, but by splitting them into constituent parts, we could try/catch around only the Assert() looking for AssertionExceptions, for example).

Where data is concerned, a unit test is composed of many initializers, many assertions and one action. Notice that each of these components do not exclusively belong to a single unit test instance, but are shared among them, promoting reuse.

Implementing this model quickly showed how far you can stray from documentation in just a short while. I’m going to skip posting and explaining the implementation (but I’ll supply a link at some point with the code when I’m happy with the result). Instead, I’ll share the bit that really matters: how it looks to clients.

Under my CalculatorTests project, I have one file called Initializers.cs (for all Calculator preconditions) one file called Assertions.cs (for all Calculator assertions) and then a file called CalculatorTests.cs which contains all of the unit tests pertaining to the Calculator class. Note that this is the only file that people need look in to discern the intent of the code.

Initializers:

C#
public class CalculatorIsDefaultConstructed : IInitializer<ICalculator>
{
	public void Prepare(ref ICalculator target)
	{
		target = new Calculator();
	}
}

Assertions:

C#
public class ResultShouldEqual : IAssertion<ICalculator, int>
{
	private int expectedValue;

	public ResultShouldEqual(int expectedValue)
	{
		this.expectedValue = expectedValue;
	}

	public void Verify(ICalculator target, int returnValue)
	{
		Assert.IsNotNull(target);
		Assert.AreEqual(this.expectedValue, returnValue);
	}
}

public class ExceptionThrown<TException> : IAssertion<Exception>
	where TException : Exception
{
	public void Verify(Exception exceptionThrown)
	{
		Assert.IsInstanceOf(typeof(TException), exceptionThrown);
	}
}

The unit tests now look like this:

C#
public class CanAddTwoPositiveNumbers : UnitTest<ICalculator, int>
{
	public CanAddTwoPositiveNumbers()
	{
		GivenThat<CalculatorIsDefaultConstructed>();
		When(calculator => calculator.Add(13, 45));
		Then<ResultShouldEqual>(58);
	}
}

public class OverflowCausesException : UnitTest<ICalculator, int>
{
	public OverflowCausesException()
	{
		GivenThat<CalculatorIsDefaultConstructed>();
		When(calculator => calculator.Add(int.MaxValue, 1));
		ThenThrow<OverflowException>();
	}
}

So, each unit test has its Arrange, Act, Assert components clearly visible as well-named classes, but the implementation of each is hidden away elsewhere. It can be inferred what CalculatorIsDefaultConstructed does, so you don’t need to see its guts. The almost English-language specification of the test is quite nice, too. I’m going to add a fluent interface to the initialization and assertion registration, to include And(). This is now, for all intents and purposes, Behavior Driven Development (BDD)…

There are a couple of issues with this approach, still. Mainly, it is overkill for the example presented here. The initializers and assertions made aren’t complex enough to warrant their own classes and the readability of the intent isn’t sufficient reason to justify the extra fingerwork.

I’m about to write a non-trivial suite of unit tests using this organization and I will report back with my findings and – by then – some kind. The implementation of UnitTest is integrated into NUnit so I can use its framework wherever required.

How do you organize your unit tests?

Image 2 Image 3 Image 4 Image 5 Image 6 Image 7 Image 8 Image 9

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Nephila Capital Ltd.
Bermuda Bermuda
An experienced .NET developer, currently working for Nephila Capital Ltd. in Bermuda. Author of "Pro WPF and Silverlight MVVM".

Comments and Discussions

 
QuestionAbout project Pin
@certijo26-May-16 4:52
@certijo26-May-16 4:52 
QuestionSeems like Overkill... why not use the .Net BDD tool variants directly ? Pin
Gishu Pillai1-Aug-11 20:20
Gishu Pillai1-Aug-11 20:20 
AnswerRe: Seems like Overkill... why not use the .Net BDD tool variants directly ? Pin
garymcleanhall1-Aug-11 22:56
garymcleanhall1-Aug-11 22:56 
GeneralRe: Seems like Overkill... why not use the .Net BDD tool variants directly ? Pin
Gishu Pillai1-Aug-11 23:36
Gishu Pillai1-Aug-11 23:36 
GeneralRe: Seems like Overkill... why not use the .Net BDD tool variants directly ? Pin
Russell Lear2-Aug-11 11:11
Russell Lear2-Aug-11 11:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.