【问题标题】:How to test method call order with Moq如何使用 Moq 测试方法调用顺序
【发布时间】:2010-12-18 10:54:32
【问题描述】:

目前我有:

    [Test]
    public void DrawDrawsAllScreensInTheReverseOrderOfTheStack() {
        // Arrange.
        var screenMockOne = new Mock<IScreen>();
        var screenMockTwo = new Mock<IScreen>();
        var screens = new List<IScreen>();
        screens.Add(screenMockOne.Object);
        screens.Add(screenMockTwo.Object);
        var stackOfScreensMock = new Mock<IScreenStack>();
        stackOfScreensMock.Setup(s => s.ToArray()).Returns(screens.ToArray());
        var screenManager = new ScreenManager(stackOfScreensMock.Object);
        // Act.
        screenManager.Draw(new Mock<GameTime>().Object);
        // Assert.
        screenMockOne.Verify(smo => smo.Draw(It.IsAny<GameTime>()), Times.Once(),
            "Draw was not called on screen mock one");
        screenMockTwo.Verify(smo => smo.Draw(It.IsAny<GameTime>()), Times.Once(), 
            "Draw was not called on screen mock two");
    }

但是我在生产代码中绘制对象的顺序并不重要。我可以先做一个,或者两个没关系。不过这应该很重要,因为抽签顺序很重要。

您如何(使用 Moq)确保按特定顺序调用方法?

编辑

我摆脱了那个测试。 draw 方法已从我的单元测试中删除。我只需要手动测试它是否有效。虽然顺序的颠倒被带入了一个单独的测试类,在那里它被测试了,所以它并不全是坏的。

感谢您提供有关他们正在研究的功能的链接。我当然希望它尽快添加,非常方便。

【问题讨论】:

标签: c# unit-testing moq


【解决方案1】:

否则,您可以使用回调函数并增加/存储 callIndex 值。

【讨论】:

    【解决方案2】:

    使用 Moq CallBacks 的简单解决方案:

        [TestMethod]
        public void CallInOrder()
        {
            // Arrange
            string callOrder = "";
    
            var service = new Mock<MyService>();
            service.Setup(p=>p.FirstCall()).Returns(0).CallBack(()=>callOrder += "1");
            service.Setup(p=>p.SecondCall()).Returns(0).CallBack(()=>callOrder += "2");
    
            var sut = new Client(service);
    
            // Act
            sut.DoStuff();
    
            // Assert
            Assert.AreEqual("12", callOrder);
        }
    

    【讨论】:

    • 不起作用,它说它无法从用法中推断出类型
    • @DaveH 你试过调试吗?哪条线路有问题?
    • @DaveH 代替 "p=>callOrder" 使用 "()=>callOrder"
    【解决方案3】:

    从原始帖子我可以假设测试方法执行以下操作调用:

    var screenOne = new Screen(...);
    var screenTwo = new Screen(...);
    var screens = new []{screenOne, screenTwo};
    var screenManager = new ScreenManager(screens);
    screenManager.Draw();
    

    “Draw”方法的实现是这样的:

    public class ScreenManager
    {
        public void Draw()
        {
            _screens[0].Draw();
            _screens[1].Draw();
        }
    }
    

    在我看来,如果调用顺序非常重要,那么应该在系统中引入额外的结构(描述顺序)。

    最简单的实现:每个屏幕都应该知道他的后续元素,并在自己绘制后调用其Draw方法:

    // 1st version
    public class Screen(Screen screenSubSequent)
    {
        private Screen _screenNext;
        public Screen(Screen screenNext)
        {
            _screenNext=screenNext;
        }
        public void Draw()
        {
            // draw himself
            if ( _screenNext!=null ) _screenNext.Draw();
        }
    }
    
    public class ScreenManager
    {
        public void Draw()
        {
            _screens[0].Draw();
        }
    }
    
    static void Main()
    {
        var screenOne = new Screen(null, ...);
        var screenTwo = new Screen(screenOne, ...);
        var screens = new []{screenOne, screenTwo};
        var screenManager = new ScreenManager(screens);
    }
    

    从这一点来看,每个 Screen 元素都应该对另一个元素有所了解。这并不总是好的。如果是这样:您可以创建一些类似“ScreenDrawer”的类。这个对象会存储自己的屏幕和后续的屏幕(可能继承自Screen类。使用其他世界:'ScreenDrawer'类描述系统结构。这是一个最简单的实现场景:

    // 2nd version
    public class ScreenDrawer
    {
        private Screen _screenNext;
        public ScreenDrawer(Screen screenNext, ...) : base (...)
        {
            _screenNext=screenNext;
        }
        public void Draw()
        {
            // draw himself
            if ( _screenNext!=null ) _screenNext.Draw();
        }
    }
    
    public class ScreenManager
    {
        public void Draw()
        {
            _screens[0].Draw();
        }
    }
    
    static void Main()
    {
        var screenOne = new ScreenDrawer(null, ...);
        var screenTwo = new ScreenDrawer(screenOne, ...);
        var screens = new []{screenOne, screenTwo};
        var screenManager = new ScreenManager(screens);
    }
    

    第二个方法引入了额外的继承,但不需要 Screen 类知道他的子序列元素。

    总结:这两种方法都进行子顺序调用并且不需要“顺序”测试。相反,他们需要测试当前的“屏幕”是否调用另一个屏幕,并测试“屏幕管理器”是否依次调用第一个元素的“绘制”方法。

    这种方法:

    1. 可测试性更强(可以使用大多数测试框架实现,无需支持“序列测试”);
    2. 更稳定(没有人可以轻松更改序列:您不仅需要更新源代码,还需要更新一些测试);
    3. 更加面向对象(您使用的是对象,而不是像“序列”这样的抽象实体);
    4. 因此:更受支持。

    谢谢。

    【讨论】:

      【解决方案4】:

      我最近创建了 Moq.Sequences,它提供了在 Moq 中检查订购的能力。您可能想阅读我的post,其中描述了以下内容:

      • 支持方法调用、属性 setter 和 getter。
      • 允许您指定数量 特定呼叫应该是 预计。
      • 提供循环,让您可以 将呼叫分组到一个循环组中。
      • 允许您指定编号 应该循环的次数。
      • 预期会被调用的调用 按顺序可以与 预期按任何顺序调用。
      • 多线程支持。

      典型用法如下:

      [Test]
      public void Should_show_each_post_with_most_recent_first_using_sequences()
      {
          var olderPost = new Post { DateTime = new DateTime(2010, 1, 1) };
          var newerPost = new Post { DateTime = new DateTime(2010, 1, 2) };
          var posts = new List<Post> { newerPost, olderPost };
      
          var mockView = new Mock<BlogView>();
      
          using (Sequence.Create())
          {
              mockView.Setup(v => v.ShowPost(newerPost)).InSequence();
              mockView.Setup(v => v.ShowPost(olderPost)).InSequence();
      
              new BlogPresenter(mockView.Object).Show(posts);
          }
      }
      

      【讨论】:

      • 这是一个很棒的起订量扩展;对我来说,缺乏序列支持是项目中的主要遗漏。 IMO 这应该被拉进后备箱(github.com/dwhelan/Moq-Sequences)。
      • 如何在一个序列中对不同的起订量对象执行调用 - 是否支持?
      • @chester89 是的,支持
      【解决方案5】:

      目前似乎尚未实施。见Issue 24: MockSequenceThis thread 讨论这个问题。

      不过,您可能会考虑修改您的测试。我一般觉得测试顺序会导致测试脆弱,因为它经常测试实现细节。

      编辑:我不确定这是否解决了 OP 的问题。 Lucero 的回答可能更有帮助。

      【讨论】:

      • 断言/验证顺序可能会导致脆弱的测试,但不是因为它正在测试实现细节。如果您使用 Mocks,尤其是使用控制反转(依赖注入)模式的类,您已经在测试实现细节;这就是我想说的。我想说的是,测试顺序不应该是您的测试中的常见模式,但在某些有效情况下,没有其他方法可以使用 TDD 驱动正确的代码。
      • @JesseWebb - 我认为我们只是在语义上存在分歧。让我们同意一些测试对内部实现细节有不适当的了解。我更喜欢在重构被测系统时不会中断的测试,只要公共 API 保持不变。如您所说,有测试顺序的情况,但并不常见。
      • 你所描述的测试(只关心被测系统的API)和使用mock的测试的区别在于,如果类没有依赖,前者只能是真正的单元测试.如果该类有任何依赖项并且您没有使用模拟,那么这些就是功能测试。我同意黑盒和白盒测试都有其价值。
      • @JesseWebb - 我认为这不是进行扩展讨论的合适论坛。我说的是在模拟依赖项时孤立地进行单元测试。接口依赖项是公共 API 的一部分。
      • 在我的例子中,我想在使用数据库的服务之前测试数据库是否已连接。虽然我正在尝试测试一些实现细节,但我使用数据库的服务似乎不太可能在没有先打开连接的情况下使用数据库。
      【解决方案6】:

      看看这个blog post,它可能会解决你的问题。

      【讨论】:

      • 在我的情况下无法使用它,我无法使用队列来交换数组。不过现在也无所谓了。见编辑。
      猜你喜欢
      • 1970-01-01
      • 2020-08-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多