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”.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;
}
}
[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);
}
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;
}
}
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 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);
}
[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…
No comments:
Post a Comment