Moq第一次和第二次返回值不同


262

我有一个像这样的测试:

    [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

Answers:


452

使用最新版本的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);

2
好的答案,唯一的限制是“ SetupSequence”不适用于受保护的成员。
Chasefornone

7
las,SetupSequence()不适用于Callback()。如果确实如此,则可以“状态机”方式验证对模拟方法的调用。
urig

@stackunderflow SetupSequence仅适用于两个调用,但是如果需要两个以上的调用怎么办?
TanvirArjel

@TanvirArjel,不确定您的意思... SetupSequence可用于任意数量的通话。我给出的第一个示例返回了5个调用序列。
stackunderflow

@stackunderflow抱歉!这是我的误会!是! 您是正确的,它按预期工作!
TanvirArjel

115

现有的答案很不错,但是我认为我会选择一种替代方法,该替代方法仅使用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);

谢谢。我只是修正了我将pageModel模拟而不是pageModel.Object入队的错字,所以现在它甚至也应该构建!:)
莫。

3
答案是正确的,但是请注意,如果您想扔掉它,那将是Exception行不通的Enqueue。但是SetupSequence可以工作(例如,参见@stackunderflow的答案)。
哈佛2014年

4
您必须对出队使用委托的方法。编写样本的方式将始终重复返回队列中的第一项,因为出队是在设置时评估的。
杰森·科恩

7
那是一位代表。如果代码包含Dequeue()而不是just Dequeue,那将是正确的。
莫。

31

添加回调对我不起作用,我改用了这种方法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);
    }

29

设置模拟对象时可以使用回调。看看Moq Wiki(http://code.google.com/p/moq/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();
            });

1
每次执行此操作时,我都会得到null:var pageModel = new Mock <IPageModel>(); IPageModel模型= null; repository.Setup(x => x.GetPageByUrl <IPageModel>(path))。Returns(()=> model).Callback(()=> {model = pageModel.Object;});
marcus

GetPageByUrl是否在resolver.ResolvePath方法内调用两次?

ResolvePath包含下面的代码,但两次均无效。var foo = _repository.GetPageByUrl <IPageModel>(virtualUrl); var foo2 = _repository.GetPageByUrl <IPageModel>(virtualUrl);
marcus

2
确认回调方法无效(即使在Moq的早期版本中也尝试过)。另一种可能的方法-取决于您的测试-只是Setup()再次执行调用,并Return()使用不同的值。
肯特·布加特


4

对于相同类型的问题,此处的要求略有不同。
我需要从基于不同输入值的模拟中获得不同的返回值,并找到一种解决方案,因为它使用Moq的声明性语法(从Linq到Mocks),因此IMO更具可读性。

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

对我而言(此处为Moq 4.13.0,从2019年开始),即使使用更短的da.GetFromDb(0) == new Account { ..None.. && da.GetFromDb(1) == new Account { InActive } && ...,完全不需要It.Islambda的代码,它也可以正常工作。
ojdo

3

接受的答案,还有SetupSequence答案,把手返回常量。

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

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如果需要的话,等等)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.