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

时间:2015-01-03 12:40:00

标签: c# unit-testing events lambda 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));
        }

    }

示例用法如下:

    [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被触发了两次,但是当我逐步完成代码时,显然FuelFilledFuelChanged之后触发了FuelChanged只被解雇一次。

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

        var subscriptions = subscribeActions.ToList();

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

然而结果是相同的,被触发包含{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);
        }
    }
}

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

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

更新2:

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

2 个答案:

答案 0 :(得分:4)

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

首先是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);
        }
    }      
}

由于缺少事件处理程序的代码,我假设使用它。正如评论在前面的代码块中所描述的那样,普通代表可以更轻松地完成。这只是一个选择问题,我认为这个实现不是最好的,但适合调试:

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

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

[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( "enter image description here", 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}。

触发的最后一个事件是 FuelNoLongerEmpty 事件,而且这个事件未通过测试。

<强> 注意:
由于这段代码还没有提供一个原始问题的答案,因为lambda可能会引起一些干扰,正如我上面提供的代码所做的那样,做正确的事。

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

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

因此,Nick的原始问题中的问题可能是由于您通过队列进行枚举而引起的。枚举并直接将它们传递给lambda表达式时,您将使用引用。一个技巧可能是通过将其复制到迭代循环范围内的局部变量来实际取消引用它。这正是 smiech 在他的帖子中提到的内容。

修改

我刚刚再次为你调查。你确定你所遇到的“挑战”不仅仅是将被解雇的词典的索引与expectedSequence进行比较。事件是否按逆序发生?请注意,队列是基于FIFO的,因此当出队时,它将检索插入的第一个......

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

当我在原始

中提供的代码中更改一个语句时
 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);
   }

然后测试中的所有内容都会完美无缺,正如您在此屏幕截图中看到的那样:

{{0}}

答案 1 :(得分:2)

第一部分:是的,它与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);
}