I have a helper method for my unit tests that asserts that a specific sequence of events were raised in a specific order. The code is as follows:
public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions, Action triggerAction)
{
var expectedSequence = new Queue<int>();
for (int i = 0; i < subscribeActions.Count; i++)
{
expectedSequence.Enqueue(i);
}
ExpectEventSequence(subscribeActions, triggerAction, expectedSequence);
}
public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions, Action triggerAction, Queue<int> expectedSequence)
{
var fired = new Queue<int>();
var actionsCount = subscribeActions.Count;
for(var i =0; i< actionsCount;i++)
{
subscription((o, e) =>
{
fired.Enqueue(i);
});
}
triggerAction();
var executionIndex = 0;
var inOrder = true;
foreach (var firedIndex in fired)
{
if (firedIndex != expectedSequence.Dequeue())
{
inOrder = false;
break;
}
executionIndex++;
}
if (subscribeActions.Count != fired.Count)
{
Assert.Fail("Not all events were fired.");
}
if (!inOrder)
{
Assert.Fail(string.Format(
CultureInfo.CurrentCulture,
"Events were not fired in the expected sequence from element {0}",
executionIndex));
}
}
Example usage is as follows:
[Test()]
public void FillFuel_Test([Values(1, 5, 10, 100)]float maxFuel)
{
var fuelTank = new FuelTank()
{
MaxFuel = maxFuel
};
var eventHandlerSequence = new Queue<Action<EventHandler>>();
eventHandlerSequence.Enqueue(x => fuelTank.FuelFull += x);
//Dealing with a subclass of EventHandler
eventHandlerSequence.Enqueue(x => fuelTank.FuelChanged += (o, e) => x(o, e));
Test.ExpectEventSequence(eventHandlerSequence, () => fuelTank.FillFuel());
}
And the code under test:
public float Fuel
{
get
{
return fuel;
}
private set
{
var adjustedFuel = Math.Max(0, Math.Min(value, MaxFuel));
if (fuel != adjustedFuel)
{
var oldFuel = fuel;
fuel = adjustedFuel;
RaiseCheckFuelChangedEvents(oldFuel);
}
}
}
public void FillFuel()
{
Fuel = MaxFuel;
}
private void RaiseCheckFuelChangedEvents(float oldFuel)
{
FuelChanged.FireEvent(this, new FuelEventArgs(oldFuel, Fuel));
if (fuel == 0)
{
FuelEmpty.FireEvent(this, EventArgs.Empty);
}
else if (fuel == MaxFuel)
{
FuelFull.FireEvent(this, EventArgs.Empty);
}
if (oldFuel == 0 && Fuel != 0)
{
FuelNoLongerEmpty.FireEvent(this, EventArgs.Empty);
}
else if (oldFuel == MaxFuel && Fuel != MaxFuel)
{
FuelNoLongerFull.FireEvent(this, EventArgs.Empty);
}
}
So the test expects FuelFilled
to be fired before FuelChanged
but in actuality FuelChanged
is fired first, which fails the test.
However my test is instead reporting that FuelChanged
is being fired twice, but when I step through the code it is clear that FuelFilled
is fired after FuelChanged
and FuelChanged
is only fired once.
I assumed that it was something to do with the way lambdas work with local state, maybe the for loop iterator variable was only ever set to the final value, so I replaced the for loop with this:
var subscriptions = subscribeActions.ToList();
foreach (var subscription in subscriptions)
{
subscription((o, e) =>
{
var index = subscriptions.IndexOf(subscription);
fired.Enqueue(index);
});
}
However the result is the same, fired contains {1;1} instead of {1;0}.
Now I'm wondering if the same lambda is being assigned to both events instead of using the different subscription / index state. Any ideas?
Update: I was unable to get success with either answer posted so far (same as my initial results), despite their similarities to my actual code, so I presume the issue is located elsewhere in my FuelTank
code. I've pasted the full code for FuelTank
below:
public class FuelTank
{
public FuelTank()
{
}
public FuelTank(float initialFuel, float maxFuel)
{
MaxFuel = maxFuel;
Fuel = initialFuel;
}
public float Fuel
{
get
{
return fuel;
}
private set
{
var adjustedFuel = Math.Max(0, Math.Min(value, MaxFuel));
if (fuel != adjustedFuel)
{
var oldFuel = fuel;
fuel = adjustedFuel;
RaiseCheckFuelChangedEvents(oldFuel);
}
}
}
private float maxFuel;
public float MaxFuel
{
get
{
return maxFuel;
}
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("MaxFuel", value, "Argument must be not be less than 0.");
}
maxFuel = value;
}
}
private float fuel;
public event EventHandler<FuelEventArgs> FuelChanged;
public event EventHandler FuelEmpty;
public event EventHandler FuelFull;
public event EventHandler FuelNoLongerEmpty;
public event EventHandler FuelNoLongerFull;
public void AddFuel(float fuel)
{
Fuel += fuel;
}
public void ClearFuel()
{
Fuel = 0;
}
public void DrainFuel(float fuel)
{
Fuel -= fuel;
}
public void FillFuel()
{
Fuel = MaxFuel;
}
private void RaiseCheckFuelChangedEvents(float oldFuel)
{
FuelChanged.FireEvent(this, new FuelEventArgs(oldFuel, Fuel));
if (fuel == 0)
{
FuelEmpty.FireEvent(this, EventArgs.Empty);
}
else if (fuel == MaxFuel)
{
FuelFull.FireEvent(this, EventArgs.Empty);
}
if (oldFuel == 0 && Fuel != 0)
{
FuelNoLongerEmpty.FireEvent(this, EventArgs.Empty);
}
else if (oldFuel == MaxFuel && Fuel != MaxFuel)
{
FuelNoLongerFull.FireEvent(this, EventArgs.Empty);
}
}
}
FuelEventArgs
looks like this:
public class FuelEventArgs : EventArgs
{
public float NewFuel
{
get;
private set;
}
public float OldFuel
{
get;
private set;
}
public FuelEventArgs(float oldFuel, float newFuel)
{
this.OldFuel = oldFuel;
this.NewFuel = newFuel;
}
}
The FireEvent
extension method is looks like this:
public static class EventHandlerExtensions
{
/// <summary>
/// Fires the event. This method is thread safe.
/// </summary>
/// <param name="handler"> The handler. </param>
/// <param name="sender"> Source of the event. </param>
/// <param name="args"> The <see cref="EventArgs"/> instance containing the event data. </param>
public static void FireEvent(this EventHandler handler, object sender, EventArgs args)
{
var handlerCopy = handler;
if (handlerCopy != null)
{
handlerCopy(sender, args);
}
}
/// <summary>
/// Fires the event. This method is thread safe.
/// </summary>
/// <typeparam name="T"> The type of event args this handler has. </typeparam>
/// <param name="handler"> The handler. </param>
/// <param name="sender"> Source of the event. </param>
/// <param name="args"> The <see cref="EventArgs"/> instance containing the event data. </param>
public static void FireEvent<T>(this EventHandler<T> handler, object sender, T args) where T : EventArgs
{
var handlerCopy = handler;
if (handlerCopy != null)
{
handlerCopy(sender, args);
}
}
}
The full test code can be found above in the question, there is no other code called during test execution.
I am using NUnit test framework via the Unity Testing Tools plugin for the Unity3D engine, .NET version 3.5 (ish, it's closer to Mono 2.0, I believe), and Visual Studio 2013.
Update 2:
After extracting the code and tests to their own project (outside of the Unity3D ecosystem) all tests run as expected, so I'm going to have to chalk this one up to a bug in the Unity -> Visual Studio bridge.