Introduction
Unit testing is gaining followers in the developer community. The ability to run tests on your code in a fast and reliable form, costing almost nothing to run over and over, is very easy to justify in the long run. Unit tests are code methods that run specific portions of the production code, typically a single method every time, while ensuring to execute as many possible angles of the tested code, by setting up the input and initial state, and then validating the output and end state are as expected. Several scenarios are run one after the other to test a specific method in multiple scenarios.
There are many mocking frameworks and solutions are out there, such as NUnit, Moq, RhinoMock, and many others.
Background
Let's have a look at the following class we want to test:
public class StateMachine<T>
{
T _currentState;
public void SetState(T newState)
{
if (newState.Equals(_currentState) || newState.Equals(default(T)))
{
return;
}
fireLeaveStatEvent(_currentState);
fireEnterStatEvent(newState);
_currentState = newState;
}
public T CurrentState
{
get
{
if (_currentState == null)
{
return default(T);
}
return _currentState;
}
}
public event Action<StateMachine<T>, T> LeaveState;
protected virtual void fireLeaveStatEvent(T oldState)
{
Action<StateMachine<T>, T> handler = null;
handler = LeaveState;
if (handler == null || oldState == null)
{ return; }
handler(this, oldState);
}
public event Action<StateMachine<T>, T> EnterState;
protected virtual void fireEnterStatEvent(T newState)
{
Action<StateMachine<T>, T> handler = null;
handler = EnterState;
if (handler == null || newState == null)
{ return; }
handler(this, newState);
}
}
Let's start writing tests for this class. At first, we can write up a basic test scenario to test set state value:
[test]
public void TestMethod
{
var target = new TestStateMachine();
target.SetState(1);
int current = target.CurrentState;
Assert.AreEqual(1, current );
}
After this, we should write up other scenarios to cover special cases, like setting the same value, or a null
state.
But what about monitoring the events fired by the StateMachine
? I've read many discussions on firing events from mocks, but few on the other side.
One approach asks that you create your own custom delegate:
[test]
public void TestMethod
{
bool called = false;
var target = new TestStateMachine();
target.EnterEvent += (x,y) => called = true;
target.SetState(1);
Assert.AreEqual(true, called );
}
This is fairly easy to do once, but is not very reusable. In general, we want our test code to run and monitor the code as it is written and not be too aware of any implementation details although it should say that there are some exceptions to this rule.
Having said that, one could suggest that we refactor the StateMachine
class to take a subscriber Interface, and then pass on a Mock
object that contains one form of call monitoring/enforcing or another. But this would force us never to use Events. So that stays a bad idea.
[I saw an interesting approach here and I was inspired.]
Using the Code
This is where the MockEventSubscriber
comes in and saves the day. The version here is a very basic implementation, but it will be obviously easy to expand.
public class MockEventSubscriber
{
public MockEventSubscriber()
{
Reset();
}
public void Reset()
{
_count = 0;
}
public void Handle()
{
_count++;
}
public void Handle<T>(T t)
{
_count++;
}
public void Handle<T, U>(T t, U u)
{
_count++;
}
public int HitCount
{
get
{
return _count;
}
}
int _count;
}
This class will monitor calls to its methods. Every call to one of its methods is recorded in the hit counter. Since all of the Handle
methods take generic parameters, the delegate signature is automatically matched to any method with 0-2 parameters. Now we can use it in a Test as such:
[test]
public void TestMethod
{
var target = new TestStateMachine();
var subscriber = new MockEventSubscriber();
target.LeaveState += subscriber.Handle;
target.EnterState += subscriber.Handle;
target.SetState(1);
Assert.AreEqual(0, subscriber.HitCount);
}
This implementation can be enhanced, for example to monitor order across multiple events. But this already enables to us to Assert
the number of calls as expected.