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

Nic*_*ell 7 c# events lambda unit-testing anonymous-function

我有一个帮助方法,用于我的单元测试,断言特定顺序的事件是按特定顺序引发的.代码如下:

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

    }
Run Code Online (Sandbox Code Playgroud)

示例用法如下:

    [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());
    }
Run Code Online (Sandbox Code Playgroud)

而且正在测试的代码:

    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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

因此测试期望FuelFilled在之前被解雇,FuelChanged但实际上先被解雇,但未FuelChanged通过测试.

然而,我的测试是报告FuelChanged被触发两次,但是当我逐步执行代码时,很明显FuelFilled是在之后触发FuelChanged并且FuelChanged只触发一次.

我认为这与lambdas使用本地状态的方式有关,也许for循环迭代器变量只被设置为最终值,所以我用这个取代了for循环:

        var subscriptions = subscribeActions.ToList();

        foreach (var subscription in subscriptions)
        {
            subscription((o, e) =>
                {
                    var index = subscriptions.IndexOf(subscription);
                    fired.Enqueue(index);
                });
        }
Run Code Online (Sandbox Code Playgroud)

但结果是相同的,被触发包含{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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

完整的测试代码可以在上面的问题中找到,在测试执行期间没有其他代码被调用.

我通过Unity测试工具插件使用NUnit测试框架,Unity3D引擎,.NET版本3.5(是的,它更接近Mono 2.0,我相信)和Visual Studio 2013.

更新2:

在将代码和测试提取到他们自己的项目之后(在Unity3D生态系统之外),所有测试都按预期运行,因此我将不得不将这个问题归结为Unity - > Visual Studio桥中的错误.

Rvd*_*V79 4

根据尼克的问题,我有以下实现。

首先是 FuelTank 的类:

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);
        }
    }      
}
Run Code Online (Sandbox Code Playgroud)

由于事件处理程序的代码丢失,我假设使用它。正如前面代码块中的注释所描述的,使用普通委托可以更轻松地完成此操作。这只是一个选择问题,我认为这个实现还不是最好的,但足够适合调试:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

为了测试这一切,我使用了这个类,其中包含原始问题的大部分代码:

[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());
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,当使用 NUnit 运行测试时,我注意到以下结果:

触发的第一个事件是FuelChanged事件,这会导致方法内触发队列

public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions, Action triggerAction, Queue<int> expectedSequence)
Run Code Online (Sandbox Code Playgroud)

包含{1}。

触发的下一个事件是FuelFull事件,这意味着触发的队列现在包含: {1,0},正如 Nick 的问题所预期的那样。

最后触发的事件是FuelNoLongerEmpty事件,该事件未通过测试。

注意:
由于此代码尚未提供 lambda 可能会造成一些干扰这一事实的原始问题的答案,因此我上面提供的代码做了正确的事情。

以下规则适用于 lambda 表达式中的变量范围:

  • 捕获的变量不会被垃圾收集,直到引用它的委托超出范围。
  • lambda 表达式中引入的变量在
    外部方法中不可见。
  • lambda 表达式无法直接捕获 ref 或 out 参数
    lambda 表达式无法直接从封闭方法
  • lambda 表达式中的 return 语句不会导致
    封闭方法返回。
  • lambda 表达式不能包含目标位于函数体外部或包含的匿名函数体中的 goto 语句、break 语句或 continue 语句。

因此,尼克最初问题中的问题可能是由您枚举队列这一事实引起的。在枚举并将它们直接传递给 lambda 表达式时,您将使用引用。一个技巧可能是通过将其复制到迭代循环范围内的局部变量来实际取消引用它。这正是斯米奇在他的帖子中所指的内容。

编辑:

我刚刚又帮你查了一下。您确定您遇到的“挑战”不仅仅是将已触发字典的索引与预期序列进行比较的事实。Dequeue 是以相反的顺序发生的吗?请注意,队列是基于 FIFO 的,因此在出队时,它将检索第一个插入的...

我注意到(根据我的代码)触发的字典包含{1,0},而expectedSequence字典包含{0,1}。通过查看预期事件,这对于预期序列队列来说是有好处的。因此,实际上触发的队列(填充在最后一个代码块中)是通过事件处理程序的“年龄”错误地构建的。

当我更改您在原始代码中提供的代码中的一条语句时

 public static void ExpectEventSequence(Queue<Action<EventHandler>> subscribeActions, Action triggerAction, Queue<int> expectedSequence)
Run Code Online (Sandbox Code Playgroud)

方法来自

  var subscriptions = subscribeActions.ToList();

  foreach (var firedIndex in fired)
  {

    if (firedIndex != expectedSequence.Dequeue())
    {
       inOrder = false;
       break;
    }

    executionIndex++;
    Console.WriteLine("Execution index: {0}", executionIndex);
  }
Run Code Online (Sandbox Code Playgroud)

对此:

   //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);
   }
Run Code Online (Sandbox Code Playgroud)

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

在此输入图像描述