【问题标题】:Different return values the first and second time with Moq第一次和第二次使用 Moq 时返回值不同
【发布时间】:2011-11-09 09:27:42
【问题描述】:

我有一个这样的测试:

    [TestCase("~/page/myaction")]
    public void Page_With_Custom_Action(string path) {
        // Arrange
        var pathData = new Mock<IPathData>();
        var pageModel = new Mock<IPageModel>();
        var repository = new Mock<IPageRepository>();
        var mapper = new Mock<IControllerMapper>();
        var container = new Mock<IContainer>();

        container.Setup(x => x.GetInstance<IPageRepository>()).Returns(repository.Object);

        repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageModel.Object);

        pathData.Setup(x => x.Action).Returns("myaction");
        pathData.Setup(x => x.Controller).Returns("page");

        var resolver = new DashboardPathResolver(pathData.Object, repository.Object, mapper.Object, container.Object);

        // Act
        var data = resolver.ResolvePath(path);

        // Assert
        Assert.NotNull(data);
        Assert.AreEqual("myaction", data.Action);
        Assert.AreEqual("page", data.Controller);
    }

GetPageByUrl 在我的DashboardPathResolver 中运行了两次,我如何告诉 Moq 第一次返回 null 而第二次返回 pageModel.Object

【问题讨论】:

    标签: c# unit-testing nunit moq


    【解决方案1】:

    使用最新版本的 Moq(4.2.1312.1622),您可以使用 SetupSequence 设置事件序列。这是一个例子:

    _mockClient.SetupSequence(m => m.Connect(It.IsAny<String>(), It.IsAny<int>(), It.IsAny<int>()))
            .Throws(new SocketException())
            .Throws(new SocketException())
            .Returns(true)
            .Throws(new SocketException())
            .Returns(true);
    

    调用connect只会在第三次和第五次尝试成功,否则会抛出异常。

    因此,对于您的示例,它将类似于:

    repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
    .Returns(null)
    .Returns(pageModel.Object);
    

    【讨论】:

    • 不错的答案,唯一的限制是“SetupSequence”不适用于受保护的成员。
    • 唉,SetupSequence() 不适用于 Callback()。如果确实如此,则可以以“状态机”方式验证对模拟方法的调用。
    • SetupGetSetupSet 怎么样?
    • @user3613932,你能用SetupGetSetupSet 设置一个序列,其中的行为不同于一次调用到下一次调用吗?我认为这就是 @Marcus 在 2011 年提出这个问题时所要求的。
    • 这应该被标记为正确答案。提供的一些选项是有效的,但这个选项很干净并且以正确的方式使用 Moq 功能。对于回调,您可以使用 ISetupSequentialResult 接口的“CallBase”。此外,最近的 Moq 版本似乎不再支持 ReturnInOrder 方法。
    【解决方案2】:

    现有的答案很棒,但我想我会提出我的替代方案,它只使用 System.Collections.Generic.Queue 并且不需要任何关于模拟框架的特殊知识 - 因为我在编写它时没有任何知识! :)

    var pageModel = new Mock<IPageModel>();
    IPageModel pageModelNull = null;
    var pageModels = new Queue<IPageModel>();
    pageModels.Enqueue(pageModelNull);
    pageModels.Enqueue(pageModel.Object);
    

    那么……

    repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(pageModels.Dequeue);
    

    【讨论】:

    • 答案是正确的,但请注意,如果您想抛出Exception,这将不起作用,因为您不能Enqueue 它。但是SetupSequence 会起作用(例如,请参阅@stackunderflow 的答案)。
    • 您必须为 Dequeue 使用委托方法。示例的编写方式总是会重复返回队列中的第一项,因为出队是在设置时评估的。
    • 那是一个代表。如果代码包含Dequeue() 而不仅仅是Dequeue,那么您是正确的。
    【解决方案3】:

    现在您可以使用 SetupSequence。见this post

    var mock = new Mock<IFoo>();
    mock.SetupSequence(f => f.GetCount())
        .Returns(3)  // will be returned on 1st invocation
        .Returns(2)  // will be returned on 2nd invocation
        .Returns(1)  // will be returned on 3rd invocation
        .Returns(0)  // will be returned on 4th invocation
        .Throws(new InvalidOperationException());  // will be thrown on 5th invocation
    

    【讨论】:

    • 链接失效
    【解决方案4】:

    添加回调对我不起作用,我使用了这种方法而不是 http://haacked.com/archive/2009/09/29/moq-sequences.aspx,我最终得到了这样的测试:

        [TestCase("~/page/myaction")]
        [TestCase("~/page/myaction/")]
        public void Page_With_Custom_Action(string virtualUrl) {
    
            // Arrange
            var pathData = new Mock<IPathData>();
            var pageModel = new Mock<IPageModel>();
            var repository = new Mock<IPageRepository>();
            var mapper = new Mock<IControllerMapper>();
            var container = new Mock<IContainer>();
    
            container.Setup(x => x.GetInstance<IPageRepository>()).Returns(repository.Object);
            repository.Setup(x => x.GetPageByUrl<IPageModel>(virtualUrl)).ReturnsInOrder(null, pageModel.Object);
    
            pathData.Setup(x => x.Action).Returns("myaction");
            pathData.Setup(x => x.Controller).Returns("page");
    
            var resolver = new DashboardPathResolver(pathData.Object, repository.Object, mapper.Object, container.Object);
    
            // Act
            var data = resolver.ResolvePath(virtualUrl);
    
            // Assert
            Assert.NotNull(data);
            Assert.AreEqual("myaction", data.Action);
            Assert.AreEqual("page", data.Controller);
        }
    

    【讨论】:

      【解决方案5】:

      您可以在设置模拟对象时使用回调。查看 Moq Wiki (https://github.com/Moq/moq4/wiki/Quickstart) 中的示例。

      // returning different values on each invocation
      var mock = new Mock<IFoo>();
      var calls = 0;
      mock.Setup(foo => foo.GetCountThing())
          .Returns(() => calls)
          .Callback(() => calls++);
      // returns 0 on first invocation, 1 on the next, and so on
      Console.WriteLine(mock.Object.GetCountThing());
      

      您的设置可能如下所示:

      var pageObject = pageModel.Object;
      repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageObject).Callback(() =>
                  {
                      // assign new value for second call
                      pageObject = new PageModel();
                  });
      

      【讨论】:

      • 当我这样做时,我两次都得到空值: var pageModel = new Mock(); IPageModel 模型 = null; repository.Setup(x => x.GetPageByUrl(path)).Returns(() => model).Callback(() => { model = pageModel.Object; });
      • GetPageByUrl 是否在 resolver.ResolvePath 方法中调用了两次?
      • ResolvePath 包含以下代码,但两次仍然为空 var foo = _repository.GetPageByUrl(virtualUrl); var foo2 = _repository.GetPageByUrl(virtualUrl);
      • 确认回调方法不起作用(甚至在早期的 Moq 版本中尝试过)。另一种可能的方法(取决于您的测试)是再次调用Setup(),然后Return() 使用不同的值。
      【解决方案6】:

      针对相同类型的问题,要求略有不同。
      我需要根据不同的输入值从 mock 中获取不同的返回值,并找到 IMO 更具可读性的解决方案,因为它使用 Moq 的声明性语法(linq to Mocks)。

      public interface IDataAccess
      {
         DbValue GetFromDb(int accountId);  
      }
      
      var dataAccessMock = Mock.Of<IDataAccess>
      (da => da.GetFromDb(It.Is<int>(acctId => acctId == 0)) == new Account { AccountStatus = AccountStatus.None }
      && da.GetFromDb(It.Is<int>(acctId => acctId == 1)) == new DbValue { AccountStatus = AccountStatus.InActive }
      && da.GetFromDb(It.Is<int>(acctId => acctId == 2)) == new DbValue { AccountStatus = AccountStatus.Deleted });
      
      var result1 = dataAccessMock.GetFromDb(0); // returns DbValue of "None" AccountStatus
      var result2 = dataAccessMock.GetFromDb(1); // returns DbValue of "InActive"   AccountStatus
      var result3 = dataAccessMock.GetFromDb(2); // returns DbValue of "Deleted" AccountStatus
      

      【讨论】:

      • 对我来说(此处为 2019 年起订量 4.13.0),即使使用较短的 da.GetFromDb(0) == new Account { ..None.. &amp;&amp; da.GetFromDb(1) == new Account { InActive } &amp;&amp; ...,它也能正常工作,根本不需要 It.Is-lambda。
      【解决方案7】:

      accepted answerSetupSequence answer 处理返回的常量。

      Returns() 有一些有用的重载,您可以根据发送到模拟方法的参数返回一个值。根据接受的答案中给出的the solution,这是这些重载的另一种扩展方法。

      public static class MoqExtensions
      {
          public static IReturnsResult<TMock> ReturnsInOrder<TMock, TResult, T1>(this ISetup<TMock, TResult> setup, params Func<T1, TResult>[] valueFunctions)
              where TMock : class
          {
              var queue = new Queue<Func<T1, TResult>>(valueFunctions);
              return setup.Returns<T1>(arg => queue.Dequeue()(arg));
          }
      }
      

      很遗憾,使用该方法需要你指定一些模板参数,但结果还是相当可读的。

      repository
          .Setup(x => x.GetPageByUrl<IPageModel>(path))
          .ReturnsInOrder(new Func<string, IPageModel>[]
              {
                  p => null, // Here, the return value can depend on the path parameter
                  p => pageModel.Object,
              });
      

      如果需要,使用多个参数(T2T3 等)为扩展方法创建重载。

      【讨论】:

        【解决方案8】:

        我们可以简单地用int 声明一个变量作为数据类型。将其初始化为zero,然后将其值增加如下:

        int firstTime = 0;
                    repository.Setup(_ => _.GetPageByUrl<IPageModel>(path)).Returns(() =>
                    {
                        if (firstTime == 0)
                        {
                            firstTime = 1;
                            return null;
                        }
                        else if(firstTime == 1)
                        {
                            firstTime = 2;
                            return pageModel.Object;
                        }
                        else
                        {
                            return null;
                        }
                    });
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-10-02
          • 2012-12-12
          • 1970-01-01
          • 2021-06-03
          • 1970-01-01
          • 2016-10-03
          相关资源
          最近更新 更多