我有一个这样的测试:

    [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。反对第二个?


当前回答

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

其他回答

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

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

接受的答案,以及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,
        });

如果需要的话,用多个参数(T2、T3等)为扩展方法创建重载。

现有的答案是很好的,但我想我将抛出我的替代方案,它只使用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);

现在可以使用SetupSequence了。请看这篇文章。

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

这里是同样的问题,但要求略有不同。 我需要从基于不同输入值的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