预期事件序列报告重复事件的测试助手

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);
        }
    }

所以测试期望 FuelFilledFuelChanged 之前触发,但实际上 FuelChanged 先触发,这导致测试失败。

然而,我的测试报告 FuelChanged 被触发了两次,但是当我单步执行代码时,很明显 FuelFilledFuelChanged 和 [=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);
   }

然后您的测试中的所有内容都将完美通过,正如您在以下屏幕截图中看到的那样: