How to: Invoke events inside your production code using Typemock Isolator

 Using Isolate.Invoke.Event enable event driven unit testing by invoking events on fake or “real” object. If you use events to communicate between parts of your application – it’s a feature you need to know and use. In the last couple of months I have been using it a lot but there is one wall I keep on hitting – when I use Isolate.Swap to fake an instance inside my production code it’s impossible to invoke events on it – because I cannot “touch” the actual faked instance – or is it?
But first things first – let’s talk about invoking events using Typemock Isolator…

Invoking events using Isolator

Let’s say I have a special kind of calculator that due strange consequences known only to the product manager need to work via events. The calculator shell “listen” for events from an “input provider”.
image
Presented with the code below – how can you test it?

public class EventCalculator {
   public int Sum { get; private set; }
   public void RegisterInputProvider(IInputProvider provider) {
      provider.AddTwoIntegers += OnAddTwoIntegerRequested;
   }
   private void OnAddTwoIntegerRequested(object sender, AddTwoIntegersEventArgs e) {
      Sum = e.FirstNumber + e.SecondNumber;
   }
}

One way to test the code is to use Isolate.Invoke (I bet you didn’t see this one coming):

[TestMethod, Isolated]
public void AddTwoIntegers_InvokeEventWithTwoNumbers_SumEqualsSumOfNumbers() {
   var fakeIInputProvider = Isolate.Fake.Instance<IInputProvider>();
   var calculator = new EventCalculator();
   calculator.RegisterInputProvider(fakeIInputProvider);
   var eventArgs = new AddTwoIntegersEventArgs(2, 3);
   Isolate.Invoke.Event(() =>
      fakeIInputProvider.AddTwoIntegers += null, fakeIInputProvider, eventArgs);
   Assert.AreEqual(5, calculator.Sum);
}

For those of you that see event invocation for the first time it might be a little confusing. Isolator use a trick to record the event namely putting the event’s add handler call (the “+=”) inside a lambda. The other two parameters are the events parameters – “this” and “eventArgs”.
Invoking event is a simple as long as you can reach the instance that holds the event – which is not the case if you use Isolate.Swap

Invoking events on future instances

What happens if we need to invoke event on an instance that is created deep inside the production code? at work we use timers and need to test behavior dependent on timer elapsed event – just like in the next class:

public class TimerCounter {
   private readonly Timer _timer;
   public int Count { get; private set; }
   public TimerCounter() {
      _timer = new Timer(1000);
      _timer.Elapsed += OnTimerElapsed;
   }
   public void StartCounting() {
      _timer.Enabled = true;
   }
   private void OnTimerElapsed(object sender, ElapsedEventArgs e) {
      ++Count;
   }
}

It’s not the a very sophisticated class – but it was created to make a point, a timer is created and each time it invokes an event something happens – for simplicity sake I choose to increase a counter.
Testing it should be easy – we have Isolate.Swap and we know how to invoke events, so it shouldn’t be hard to come up with the following test:

[TestMethod, Isolated]
public void TimerElapsed_TimerElapsedCalledTwice_CounterEqualsTwo() {
   var fakeTimer = Isolate.Fake.Instance<Timer>();
   Isolate.Swap.NextInstance<Timer>().With(fakeTimer);
   var timerCounter = new TimerCounter();
   var fakeEventArgs = Isolate.Fake.Instance<ElapsedEventArgs>();
   Isolate.Invoke.Event(() => fakeTimer.Elapsed += null, fakeTimer, fakeEventArgs);
   Isolate.Invoke.Event(() => fakeTimer.Elapsed += null, fakeTimer, fakeEventArgs);
   Assert.AreEqual(2, timerCounter.Count);
}

The test above is simple, elegant and very wrong – it would fail each time you run it claiming that the counter is still ‘0’. So what went wrong? Swap doesn’t actually swap the next instance, it swap it’s behavior instead – meaning that when we call invoke on the fake object it doesn’t invoke event on the object inside our class. I know the good people of Typemock would add this cool feature someday but until then we have a problem – in order to invoke events we need to be able to “touch” the instance in the test.

The solution – reflection

Gur Kashi has come up with a simple solution to this issue – why not use reflection to get the field and use it to invoke the event – the cool thing about Isolator is that it can invoke events on “real” objects as well not just fakes.
With the aid of the following utility method – the class has become testable:

public static T GetField<T>(this object instance, string fieldName) {
   var fieldInfo = instance.GetType().GetField(
                                  fieldName,
                                  BindingFlags.Instance | BindingFlags.NonPublic);
   if(fieldInfo == null) {
      throw new ArgumentException(“field not found”);
   }
   return (T)fieldInfo.GetValue(instance);
}

And now in the test we can drop the fake instance and just call invoke

[TestMethod, Isolated]
public void TimerElapsed_TimerElapsedCalledTwice_CounterEqualsTwo1() {
   var timerCounter = new TimerCounter();
   var realTimer = timerCounter.GetField<Timer>(“_timer”);
   var fakeEventArgs = Isolate.Fake.Instance<ElapsedEventArgs>();
   Isolate.Invoke.Event(() => realTimer.Elapsed += null, realTimer, fakeEventArgs);
   Isolate.Invoke.Event(() => realTimer.Elapsed += null, realTimer, fakeEventArgs);
   Assert.AreEqual(2, timerCounter.Count);
}

That’s it. The only downside of this solution is that it’s string based and the test might break if the field name is changed – so make sure you have a good error message if the fields was not found.

Happy coding…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s