预期事件序列报告重复事件的测试助手
Test helper for expected events sequence reporting duplicate events
我的单元测试有一个辅助方法,它断言特定事件序列是按特定顺序引发的。代码如下:
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));
}
}
示例用法如下:
[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());
}
以及被测代码:
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);
}
}
所以测试期望 FuelFilled
在 FuelChanged
之前触发,但实际上 FuelChanged
先触发,这导致测试失败。
然而,我的测试报告 FuelChanged
被触发了两次,但是当我单步执行代码时,很明显 FuelFilled
在 FuelChanged
和 [=18 之后被触发=] 只触发一次。
我假设这与 lambda 处理本地状态的方式有关,也许 for 循环迭代器变量只被设置为最终值,所以我用这个替换了 for 循环:
var subscriptions = subscribeActions.ToList();
foreach (var subscription in subscriptions)
{
subscription((o, e) =>
{
var index = subscriptions.IndexOf(subscription);
fired.Enqueue(index);
});
}
但是结果是一样的,fired 包含 {1;1} 而不是 {1;0}。
现在我想知道是否将同一个 lambda 分配给两个事件,而不是使用不同的订阅/索引状态。有什么想法吗?
更新: 到目前为止,我无法成功发布任何一个答案(与我的初始结果相同),尽管它们与我的实际代码相似,所以我认为这个问题位于我的 FuelTank
代码的其他地方。我在下面粘贴了 FuelTank
的完整代码:
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
看起来像这样:
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;
}
}
FireEvent
扩展方法如下所示:
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);
}
}
}
完整的测试代码可以在上面的问题中找到,测试执行过程中没有调用其他代码。
我正在通过 Unity3D 引擎的 Unity 测试工具插件使用 NUnit 测试框架,.NET 版本 3.5(ish,我相信它更接近 Mono 2.0)和 Visual Studio 2013。
更新二:
提取代码并测试到他们自己的项目(在 Unity3D 生态系统之外)后,所有测试都按预期运行,所以我不得不将此归因于 Unity 中的错误 -> Visual Studio桥.
对于第一部分:是的,它与 lambdas 变量范围的方式有关。参见 Access to Modified Closure。
因为我花了一些时间试图弄明白,所以我允许自己粘贴我使用过的代码(所有测试都通过)。
class Test
{
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 subscriptions = subscribeActions.ToList();
foreach (var subscription in subscriptions)
{
subscription((o, e) =>
{
var index = subscriptions.IndexOf(subscription);
fired.Enqueue(index);
});
}
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));
}
}
public class Fueled
{
public event EventHandler<FuelEventArgs> FuelChanged = delegate { };
public event EventHandler FuelEmpty = delegate { };
public event EventHandler FuelFull = delegate { };
public event EventHandler FuelNoLongerFull = delegate { };
public event EventHandler FuelNoLongerEmpty = delegate { };
private float fuel;
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;
}
public float MaxFuel { get; set; }
private void RaiseCheckFuelChangedEvents(float oldFuel)
{
FuelChanged(this, new FuelEventArgs(oldFuel, Fuel));
if (fuel == 0)
FuelEmpty(this, EventArgs.Empty);
else if (fuel == MaxFuel)
FuelFull(this, EventArgs.Empty);
if (oldFuel == 0 && Fuel != 0)
FuelNoLongerEmpty(this, EventArgs.Empty);
else if (oldFuel == MaxFuel && Fuel != MaxFuel)
FuelNoLongerFull(this, EventArgs.Empty);
}
}
public class FuelEventArgs : EventArgs
{
public FuelEventArgs(float oldFuel, float fuel)
{
}
}
[TestFixture]
public class Tests
{
[Test()]
public void FillFuel_Test([Values(1, 5, 10, 100)]float maxFuel)
{
var fuelTank = new Fueled()
{
MaxFuel = maxFuel
};
var eventHandlerSequence = new Queue<Action<EventHandler>>();
//Dealing with a subclass of EventHandler
eventHandlerSequence.Enqueue(x => fuelTank.FuelChanged += (o, e) => x(o, e));
eventHandlerSequence.Enqueue(x => fuelTank.FuelFull += x);
Test.ExpectEventSequence(eventHandlerSequence, () => fuelTank.FillFuel());
}
}
基本上我只改变了测试方法中预期事件的顺序。如果您在更改循环后仍然得到不正确的结果,我认为问题一定在您粘贴的代码范围之外。我正在使用 VS 2013 社区 + resharper 8,nunit 2.6.4.14350
编辑:不同的方法
我试图解决您实际发布的问题,但也许这正是您想要的:
你不会考虑尝试你的方法的简化版本吗?:
[Test()]
public void FillFuel_Test([Values(1, 5, 10, 100)]float maxFuel)
{
var fuelTank = new Fueled()
{
MaxFuel = maxFuel
};
var expectedEventSequence = new[]
{
"FuelChanged",
"FuelFull"
};
var triggeredEventSequence = new List<string>();
fuelTank.FuelChanged += (o, e) => triggeredEventSequence.Add("FuelChanged");
fuelTank.FuelFull += (o, e) => triggeredEventSequence.Add("FuelFull");
fuelTank.FillFuel();
Assert.AreEqual(expectedEventSequence,triggeredEventSequence);
}
根据 Nick 的问题,我有以下实现。
首先是燃料箱的 class:
public class FuelTank
{
private float fuel;
//Basic classes for the event handling, could be done by providing a few simple delegates,
//but this is just to stick as close to the original question as possible.
public FuelChanged FuelChanged = new FuelChanged();
public FuelEmpty FuelEmpty = new FuelEmpty();
public FuelFull FuelFull = new FuelFull();
public FuelNoLongerEmpty FuelNoLongerEmpty = new FuelNoLongerEmpty();
public FuelNoLongerFull FuelNoLongerFull = new FuelNoLongerFull();
public float MaxFuel { get; set; }
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);
}
}
}
由于缺少事件处理程序的代码,我假设使用它。正如前面代码块中的评论所述,使用普通委托可以更轻松地完成。这只是一个选择问题,我认为这个实现还不是最好的,但足够适合调试:
public class FuelEventArgs : EventArgs
{
private float oldFuel, newFuel;
public FuelEventArgs(float oldFuel, float newFuel)
{
this.oldFuel = oldFuel;
this.newFuel = newFuel;
}
}
public class FuelEvents
{
public event EventHandler FireEventHandler;
public virtual void FireEvent(object sender, EventArgs fuelArgs)
{
EventHandler handler = FireEventHandler;
if (null != handler)
handler(this, fuelArgs);
}
}
public class FuelChanged : FuelEvents
{
public override void FireEvent(object sender, EventArgs fuelArgs)
{
Console.WriteLine("Fired FuelChanged");
base.FireEvent(sender, fuelArgs);
}
}
public class FuelEmpty : FuelEvents
{
public override void FireEvent(object sender, EventArgs fuelArgs)
{
Console.WriteLine("Fired FuelEmpty");
base.FireEvent(sender, fuelArgs);
}
}
public class FuelFull : FuelEvents
{
public override void FireEvent(object sender, EventArgs fuelArgs)
{
Console.WriteLine("Fired FuelFull");
base.FireEvent(sender, fuelArgs);
}
}
public class FuelNoLongerEmpty : FuelEvents
{
public override void FireEvent(object sender, EventArgs fuelArgs)
{
Console.WriteLine("Fired FuelNoLongerEmpty");
base.FireEvent(sender, fuelArgs);
}
}
public class FuelNoLongerFull : FuelEvents
{
public override void FireEvent(object sender, EventArgs fuelArgs)
{
Console.WriteLine("Fired FuelNoLongerFull");
base.FireEvent(sender, fuelArgs);
}
}
为了测试这一切,我使用了这个 class,其中包含原始问题的大部分代码:
[TestFixture]
public class Tests
{
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;
//This code has been commented out due to the fact that subscription is unknown here.
//I stuck to use the last solution that Nick provided himself
//for (var i = 0; i < actionsCount; i++)
//{
// subscription((o, e) =>
// {
// fired.Enqueue(i);
// });
//}
var subscriptions = subscribeActions.ToList();
foreach (var subscription in subscriptions)
{
subscription((o, e) =>
{
var index = subscriptions.IndexOf(subscription);
Console.WriteLine("[ExpectEventSequence] Found index: {0}", index);
fired.Enqueue(index);
});
}
triggerAction();
var executionIndex = 0;
var inOrder = true;
foreach (var firedIndex in fired)
{
if (firedIndex != expectedSequence.Dequeue())
{
inOrder = false;
break;
}
executionIndex++;
Console.WriteLine("Execution index: {0}", executionIndex);
}
if (subscribeActions.Count != fired.Count)
{
Assert.Fail("Not all events were fired.");
}
if (!inOrder)
{
Console.WriteLine("Contents of Fired Queue: {0}", PrintValues(fired));
Assert.Fail(string.Format(
CultureInfo.CurrentCulture,
"Events were not fired in the expected sequence from element {0}",
executionIndex));
}
}
private static string PrintValues(Queue<int> myCollection)
{
return string.Format( "{{0}}", string.Join(",", myCollection.ToArray()));
}
[Test()]
[ExpectedException(typeof(DivideByZeroException))]
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.FireEventHandler += x);
//Dealing with a subclass of EventHandler
eventHandlerSequence.Enqueue(x => fuelTank.FuelChanged.FireEventHandler += (o, e) => x(o, e));
ExpectEventSequence(eventHandlerSequence, () => fuelTank.FillFuel());
}
}
现在,当 运行 使用 NUnit 进行测试时,我注意到以下结果:
第一个被触发的事件是 FuelChanged 事件,这导致方法
中的触发队列
public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions, Action triggerAction, Queue<int> expectedSequence)
包含{1}。
下一个触发的事件是 FuelFull 事件,这意味着被触发的队列现在包含:
{1,0} 符合 Nick 的问题。
触发的最后一个事件是 FuelNoLongerEmpty 事件,而这个事件未通过测试。
注:
由于此代码尚未提供对 lambda 可能会造成一些干扰这一事实的原始问题的答案,正如我在上面提供的代码所做的那样。
以下规则适用于 lambda 表达式中的变量范围:
- 捕获的变量不会是garbage-collected直到
引用它的委托超出范围。
- 在 lambda 表达式中引入的变量在
中不可见
外法。
- lambda 表达式不能直接捕获 ref 或 out 参数
从一个封闭的方法。
- lambda 表达式中的 return 语句不会导致
将方法封闭到 return。
- lambda 表达式不能包含 goto 语句、break 语句、
或 continue 语句,其目标在主体外或主体内
一个包含的匿名函数。
所以 Nick 的原始问题中的问题可能是由于您枚举队列造成的。在枚举并将它们直接传递给 lambda 表达式时,您将使用引用。一个技巧可能是通过将它复制到迭代循环范围内的局部变量来实际 de-reference 它。这正是 smiech 在他的 post.
中所指的
编辑:
我刚刚为您重新调查了一遍。您确定您所拥有的 'challenge' 不仅仅是将被解雇字典的索引与 expectedSequence.Dequeue 进行比较的事实以相反的顺序发生吗?请注意,队列是基于 FIFO 的,因此在出队时,它将检索第一个插入的...
我注意到(根据我的代码)触发的字典包含 {1,0},而 expectedSequence 字典包含 {0,1}。通过查看预期事件,这对 expectedSequence 队列有好处。所以实际上触发的队列(填充在你的最后一个代码块中)是通过事件处理程序的 'age' 错误地建立的。
当我在原代码中更改了您提供的代码中的一个语句时
public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions, Action triggerAction, Queue<int> expectedSequence)
方法来自
var subscriptions = subscribeActions.ToList();
foreach (var firedIndex in fired)
{
if (firedIndex != expectedSequence.Dequeue())
{
inOrder = false;
break;
}
executionIndex++;
Console.WriteLine("Execution index: {0}", executionIndex);
}
对此:
//When comparing indexes, you'll probably need to reverse the fired queue
fired = new Queue<int>(fired.Reverse());
foreach (var firedIndex in fired)
{
if (firedIndex != expectedSequence.Dequeue())
{
inOrder = false;
break;
}
executionIndex++;
Console.WriteLine("Execution index: {0}", executionIndex);
}
然后您的测试中的所有内容都将完美通过,正如您在以下屏幕截图中看到的那样:
我的单元测试有一个辅助方法,它断言特定事件序列是按特定顺序引发的。代码如下:
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));
}
}
示例用法如下:
[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());
}
以及被测代码:
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);
}
}
所以测试期望 FuelFilled
在 FuelChanged
之前触发,但实际上 FuelChanged
先触发,这导致测试失败。
然而,我的测试报告 FuelChanged
被触发了两次,但是当我单步执行代码时,很明显 FuelFilled
在 FuelChanged
和 [=18 之后被触发=] 只触发一次。
我假设这与 lambda 处理本地状态的方式有关,也许 for 循环迭代器变量只被设置为最终值,所以我用这个替换了 for 循环:
var subscriptions = subscribeActions.ToList();
foreach (var subscription in subscriptions)
{
subscription((o, e) =>
{
var index = subscriptions.IndexOf(subscription);
fired.Enqueue(index);
});
}
但是结果是一样的,fired 包含 {1;1} 而不是 {1;0}。
现在我想知道是否将同一个 lambda 分配给两个事件,而不是使用不同的订阅/索引状态。有什么想法吗?
更新: 到目前为止,我无法成功发布任何一个答案(与我的初始结果相同),尽管它们与我的实际代码相似,所以我认为这个问题位于我的 FuelTank
代码的其他地方。我在下面粘贴了 FuelTank
的完整代码:
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
看起来像这样:
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;
}
}
FireEvent
扩展方法如下所示:
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);
}
}
}
完整的测试代码可以在上面的问题中找到,测试执行过程中没有调用其他代码。
我正在通过 Unity3D 引擎的 Unity 测试工具插件使用 NUnit 测试框架,.NET 版本 3.5(ish,我相信它更接近 Mono 2.0)和 Visual Studio 2013。
更新二:
提取代码并测试到他们自己的项目(在 Unity3D 生态系统之外)后,所有测试都按预期运行,所以我不得不将此归因于 Unity 中的错误 -> Visual Studio桥.
对于第一部分:是的,它与 lambdas 变量范围的方式有关。参见 Access to Modified Closure。 因为我花了一些时间试图弄明白,所以我允许自己粘贴我使用过的代码(所有测试都通过)。
class Test
{
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 subscriptions = subscribeActions.ToList();
foreach (var subscription in subscriptions)
{
subscription((o, e) =>
{
var index = subscriptions.IndexOf(subscription);
fired.Enqueue(index);
});
}
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));
}
}
public class Fueled
{
public event EventHandler<FuelEventArgs> FuelChanged = delegate { };
public event EventHandler FuelEmpty = delegate { };
public event EventHandler FuelFull = delegate { };
public event EventHandler FuelNoLongerFull = delegate { };
public event EventHandler FuelNoLongerEmpty = delegate { };
private float fuel;
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;
}
public float MaxFuel { get; set; }
private void RaiseCheckFuelChangedEvents(float oldFuel)
{
FuelChanged(this, new FuelEventArgs(oldFuel, Fuel));
if (fuel == 0)
FuelEmpty(this, EventArgs.Empty);
else if (fuel == MaxFuel)
FuelFull(this, EventArgs.Empty);
if (oldFuel == 0 && Fuel != 0)
FuelNoLongerEmpty(this, EventArgs.Empty);
else if (oldFuel == MaxFuel && Fuel != MaxFuel)
FuelNoLongerFull(this, EventArgs.Empty);
}
}
public class FuelEventArgs : EventArgs
{
public FuelEventArgs(float oldFuel, float fuel)
{
}
}
[TestFixture]
public class Tests
{
[Test()]
public void FillFuel_Test([Values(1, 5, 10, 100)]float maxFuel)
{
var fuelTank = new Fueled()
{
MaxFuel = maxFuel
};
var eventHandlerSequence = new Queue<Action<EventHandler>>();
//Dealing with a subclass of EventHandler
eventHandlerSequence.Enqueue(x => fuelTank.FuelChanged += (o, e) => x(o, e));
eventHandlerSequence.Enqueue(x => fuelTank.FuelFull += x);
Test.ExpectEventSequence(eventHandlerSequence, () => fuelTank.FillFuel());
}
}
基本上我只改变了测试方法中预期事件的顺序。如果您在更改循环后仍然得到不正确的结果,我认为问题一定在您粘贴的代码范围之外。我正在使用 VS 2013 社区 + resharper 8,nunit 2.6.4.14350
编辑:不同的方法
我试图解决您实际发布的问题,但也许这正是您想要的: 你不会考虑尝试你的方法的简化版本吗?:
[Test()]
public void FillFuel_Test([Values(1, 5, 10, 100)]float maxFuel)
{
var fuelTank = new Fueled()
{
MaxFuel = maxFuel
};
var expectedEventSequence = new[]
{
"FuelChanged",
"FuelFull"
};
var triggeredEventSequence = new List<string>();
fuelTank.FuelChanged += (o, e) => triggeredEventSequence.Add("FuelChanged");
fuelTank.FuelFull += (o, e) => triggeredEventSequence.Add("FuelFull");
fuelTank.FillFuel();
Assert.AreEqual(expectedEventSequence,triggeredEventSequence);
}
根据 Nick 的问题,我有以下实现。
首先是燃料箱的 class:
public class FuelTank
{
private float fuel;
//Basic classes for the event handling, could be done by providing a few simple delegates,
//but this is just to stick as close to the original question as possible.
public FuelChanged FuelChanged = new FuelChanged();
public FuelEmpty FuelEmpty = new FuelEmpty();
public FuelFull FuelFull = new FuelFull();
public FuelNoLongerEmpty FuelNoLongerEmpty = new FuelNoLongerEmpty();
public FuelNoLongerFull FuelNoLongerFull = new FuelNoLongerFull();
public float MaxFuel { get; set; }
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);
}
}
}
由于缺少事件处理程序的代码,我假设使用它。正如前面代码块中的评论所述,使用普通委托可以更轻松地完成。这只是一个选择问题,我认为这个实现还不是最好的,但足够适合调试:
public class FuelEventArgs : EventArgs
{
private float oldFuel, newFuel;
public FuelEventArgs(float oldFuel, float newFuel)
{
this.oldFuel = oldFuel;
this.newFuel = newFuel;
}
}
public class FuelEvents
{
public event EventHandler FireEventHandler;
public virtual void FireEvent(object sender, EventArgs fuelArgs)
{
EventHandler handler = FireEventHandler;
if (null != handler)
handler(this, fuelArgs);
}
}
public class FuelChanged : FuelEvents
{
public override void FireEvent(object sender, EventArgs fuelArgs)
{
Console.WriteLine("Fired FuelChanged");
base.FireEvent(sender, fuelArgs);
}
}
public class FuelEmpty : FuelEvents
{
public override void FireEvent(object sender, EventArgs fuelArgs)
{
Console.WriteLine("Fired FuelEmpty");
base.FireEvent(sender, fuelArgs);
}
}
public class FuelFull : FuelEvents
{
public override void FireEvent(object sender, EventArgs fuelArgs)
{
Console.WriteLine("Fired FuelFull");
base.FireEvent(sender, fuelArgs);
}
}
public class FuelNoLongerEmpty : FuelEvents
{
public override void FireEvent(object sender, EventArgs fuelArgs)
{
Console.WriteLine("Fired FuelNoLongerEmpty");
base.FireEvent(sender, fuelArgs);
}
}
public class FuelNoLongerFull : FuelEvents
{
public override void FireEvent(object sender, EventArgs fuelArgs)
{
Console.WriteLine("Fired FuelNoLongerFull");
base.FireEvent(sender, fuelArgs);
}
}
为了测试这一切,我使用了这个 class,其中包含原始问题的大部分代码:
[TestFixture]
public class Tests
{
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;
//This code has been commented out due to the fact that subscription is unknown here.
//I stuck to use the last solution that Nick provided himself
//for (var i = 0; i < actionsCount; i++)
//{
// subscription((o, e) =>
// {
// fired.Enqueue(i);
// });
//}
var subscriptions = subscribeActions.ToList();
foreach (var subscription in subscriptions)
{
subscription((o, e) =>
{
var index = subscriptions.IndexOf(subscription);
Console.WriteLine("[ExpectEventSequence] Found index: {0}", index);
fired.Enqueue(index);
});
}
triggerAction();
var executionIndex = 0;
var inOrder = true;
foreach (var firedIndex in fired)
{
if (firedIndex != expectedSequence.Dequeue())
{
inOrder = false;
break;
}
executionIndex++;
Console.WriteLine("Execution index: {0}", executionIndex);
}
if (subscribeActions.Count != fired.Count)
{
Assert.Fail("Not all events were fired.");
}
if (!inOrder)
{
Console.WriteLine("Contents of Fired Queue: {0}", PrintValues(fired));
Assert.Fail(string.Format(
CultureInfo.CurrentCulture,
"Events were not fired in the expected sequence from element {0}",
executionIndex));
}
}
private static string PrintValues(Queue<int> myCollection)
{
return string.Format( "{{0}}", string.Join(",", myCollection.ToArray()));
}
[Test()]
[ExpectedException(typeof(DivideByZeroException))]
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.FireEventHandler += x);
//Dealing with a subclass of EventHandler
eventHandlerSequence.Enqueue(x => fuelTank.FuelChanged.FireEventHandler += (o, e) => x(o, e));
ExpectEventSequence(eventHandlerSequence, () => fuelTank.FillFuel());
}
}
现在,当 运行 使用 NUnit 进行测试时,我注意到以下结果:
第一个被触发的事件是 FuelChanged 事件,这导致方法
中的触发队列public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions, Action triggerAction, Queue<int> expectedSequence)
包含{1}。
下一个触发的事件是 FuelFull 事件,这意味着被触发的队列现在包含: {1,0} 符合 Nick 的问题。
触发的最后一个事件是 FuelNoLongerEmpty 事件,而这个事件未通过测试。
注:
由于此代码尚未提供对 lambda 可能会造成一些干扰这一事实的原始问题的答案,正如我在上面提供的代码所做的那样。
以下规则适用于 lambda 表达式中的变量范围:
- 捕获的变量不会是garbage-collected直到 引用它的委托超出范围。
- 在 lambda 表达式中引入的变量在
中不可见 外法。 - lambda 表达式不能直接捕获 ref 或 out 参数
从一个封闭的方法。 - lambda 表达式中的 return 语句不会导致
将方法封闭到 return。 - lambda 表达式不能包含 goto 语句、break 语句、 或 continue 语句,其目标在主体外或主体内 一个包含的匿名函数。
所以 Nick 的原始问题中的问题可能是由于您枚举队列造成的。在枚举并将它们直接传递给 lambda 表达式时,您将使用引用。一个技巧可能是通过将它复制到迭代循环范围内的局部变量来实际 de-reference 它。这正是 smiech 在他的 post.
中所指的编辑:
我刚刚为您重新调查了一遍。您确定您所拥有的 'challenge' 不仅仅是将被解雇字典的索引与 expectedSequence.Dequeue 进行比较的事实以相反的顺序发生吗?请注意,队列是基于 FIFO 的,因此在出队时,它将检索第一个插入的...
我注意到(根据我的代码)触发的字典包含 {1,0},而 expectedSequence 字典包含 {0,1}。通过查看预期事件,这对 expectedSequence 队列有好处。所以实际上触发的队列(填充在你的最后一个代码块中)是通过事件处理程序的 'age' 错误地建立的。
当我在原代码中更改了您提供的代码中的一个语句时
public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions, Action triggerAction, Queue<int> expectedSequence)
方法来自
var subscriptions = subscribeActions.ToList();
foreach (var firedIndex in fired)
{
if (firedIndex != expectedSequence.Dequeue())
{
inOrder = false;
break;
}
executionIndex++;
Console.WriteLine("Execution index: {0}", executionIndex);
}
对此:
//When comparing indexes, you'll probably need to reverse the fired queue
fired = new Queue<int>(fired.Reverse());
foreach (var firedIndex in fired)
{
if (firedIndex != expectedSequence.Dequeue())
{
inOrder = false;
break;
}
executionIndex++;
Console.WriteLine("Execution index: {0}", executionIndex);
}
然后您的测试中的所有内容都将完美通过,正如您在以下屏幕截图中看到的那样: