Initializers
Initializers are the building blocks of unit tests. The whole point of Behavioral is to encourage the reuse and composition of initializing code so that it forms a readable script for setting up a test. How granular your initializers are is entirely up to you, but they can be logically grouped using InitializerCollections.

The standard initializer looks like this:

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. Furthermore, the type argument supplied here matches the overall type of the unit test in which the initializer will be used. However, this is not always useful because some initializers do not rely on any context whatsoever. Take this real-world example:

class UnityContainerIsMocked : IInitializer<SecurityCommandsUser>
{
	public void SetUp(ref SecurityCommandsUser userCommands)
	{
		var unityContainer = Isolate.Fake.Instance<IUnityContainer>();
		this.SetContext(unityContainer);
	}
}


In this case, we are mocking the Unity container - a common practice in the Prism application that I am currently working on. The problem here is obvious - we have tied ourselves to the SecurityCommandsUser class, but the set-up method completely ignores it. In the alpha release of Behavioral, this precluded reuse of such initializers (the same initializer would have to be rewritten for SecurityCommandsApplication, for example). In the beta, we do not have to tie our initializers to the targeted type of the unit test:

class UnityContainerIsMocked : IInitializer
{
	public void SetUp()
	{
		var unityContainer = Isolate.Fake.Instance<IUnityContainer>();
		this.SetContext(unityContainer);
	}
}


Much better. Now, for a little technical diversion...

The way that this works is slightly dirty, but necessary, and yields a potential problem that was turned into a feature. In .Net, generic constraints are not part of the method signature. This is by design and entirely expected behavior on behalf of the compiler. However, it's a bit of an inconvenience when you want to do something like this:

	IInitializationFluent<TTarget> GivenThat<TInitializer>(params object[] constructorArgs)
		where TInitializer : IInitializer<TTarget>;

	IInitializationFluent<TTarget> GivenThat<TInitializer>(params object[] constructorArgs)
		where TInitializer : IInitializer;


This isn't valid because the runtime isn't able to distinguish between these two methods - they are identical in its eyes. This means that we have to work around the problem, in a slightly dirty way. Long story short, any interface that can be used as an initializer is now given the marker interface IInitializerMarker. At run time, there's then some type-sniffing to discover exactly what we're dealing with and how to run it. This breaks the Open/Closed Principle, which is unfortunate, but it gives us the chance to add some more features. Firstly, we can support context-free initializers, as well as initializers that operate on the target type. However, what would have been a compile-time check - if the code above worked - is instead a runtime check. This means that you can use any TTarget value in IInitializer<TTarget> in any UnitTest. Hmm, we've opened it up a bit too far...

To make sense of this, we can leverage the Context of the test. If we assume that the target type of the test is ISession (ie: you are testing NHibernate mappings or some such), this initializer is perfectly valid:

public class TheUserIsMocked : IInitializer<User>
{
	public void SetUp(ref User user)
	{
		user = Isolate.Fake.Instance<User>();
	}
}


In this example, the User reference comes from the current unit test's Context. So, there is a valid use for an IInitializer with a target type that does not match the UnitTest's target type, meaning that the lack of compile-time check is moot, and we get some nice extra functionality.

It's also worth noting that Actions can also be used as initializers, but the reverse is not valid. When an action is used as an intiailizer, any return value is discarded.

[TestClass]
public class AddingNumbersThatCauseOverflowShouldThrowOverflowException : UnitTest<Calculator, int>
{
	[TestMethod]
	public override void Run()
	{
		GivenThat<CalculatorIsReadyToRun>()
			.And<AddingTwoNumbers>(23, 32);

		When<AddingTwoNumbers>(int.MaxValue, 1);

		ThenThrow<OverflowException>();
	}
}


Note: All initializers are called as soon as GivenThat is called from the UnitTest.

Last edited Aug 15, 2011 at 2:47 PM by garymcleanhall, version 2

Comments

No comments yet.