第一次和第二次使用 Moq 时返回值不同
Posted
技术标签:
【中文标题】第一次和第二次使用 Moq 时返回值不同【英文标题】:Different return values the first and second time with 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
?
【问题讨论】:
【参考方案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()
。如果确实如此,则可以以“状态机”方式验证对模拟方法的调用。
SetupGet
和 SetupSet
怎么样?
@user3613932,你能用SetupGet
和SetupSet
设置一个序列,其中的行为不同于一次调用到下一次调用吗?我认为这就是 @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 MockSetup()
,然后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.. && da.GetFromDb(1) == new Account InActive && ...
,它也能正常工作,根本不需要 It.Is
-lambda。【参考方案7】:
accepted answer 和 SetupSequence 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,
);
如果需要,使用多个参数(T2
、T3
等)为扩展方法创建重载。
【讨论】:
【参考方案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;
);
【讨论】:
以上是关于第一次和第二次使用 Moq 时返回值不同的主要内容,如果未能解决你的问题,请参考以下文章
oracle里数据的抽出结果排序,第一次和第二次不一样,第二次以后保持不变,啥原因?