我想要一个模拟,它第一次返回
0
,然后在此后调用该方法时返回 1
。问题是,如果该方法被调用4次,我必须这样写:
mock.SetupSequence(x => x.GetNumber())
.Returns(0)
.Returns(1)
.Returns(1)
.Returns(1);
否则,该方法返回 null。
有没有什么办法可以写成,在初次调用之后,该方法返回
1
?
最简洁的方法是创建一个
Queue
并将 .Dequeue
方法传递给 Returns
.Returns(new Queue<int>(new[] { 0, 1, 1, 1 }).Dequeue);
这不是特别花哨,但我认为它会起作用:
var firstTime = true;
mock.Setup(x => x.GetNumber())
.Returns(()=>
{
if(!firstTime)
return 1;
firstTime = false;
return 0;
});
有点晚了,但如果您仍然想使用 Moq 的 API,您可以在最后一个
Setup
调用的操作中调用 Returns
函数:
var mock = new Mock<IFoo>();
mock.SetupSequence(m => m.GetNumber())
.Returns(4)
.Returns(() =>
{
// Subsequent Setup or SetupSequence calls "overwrite" their predecessors:
// you'll get 1 from here on out.
mock.Setup(m => m.GetNumber()).Returns(1);
return 1;
});
var o = mock.Object;
Assert.Equal(4, o.GetNumber());
Assert.Equal(1, o.GetNumber());
Assert.Equal(1, o.GetNumber());
// etc...
我想使用
SetupSequence
进行演示,但对于 OP 的具体情况,您可以简化并将所有内容都放在 Setup
方法中:
mock.Setup(m => m.GetNumber())
.Returns(() =>
{
mock.Setup(m => m.GetNumber()).Returns(1);
return 4;
});
通常情况下,我不会为这样一个老问题提交新答案,但近年来 ReturnsAsync 变得非常普遍,这使得潜在的答案变得更加复杂。
正如其他人所说,您基本上可以创建一个结果队列,并在 Returns 调用中传递queue.Dequeue 委托。
例如。
var queue = new Queue<int>(new []{0,1,2,3});
mock.SetupSequence(m => m.Bar()).Returns(queue.Dequeue);
但是,如果您要设置异步方法,我们通常应该调用 ReturnsAsync。当传递到 ReturnsAsync 时,queue.Dequeue 将导致对正在设置的方法的第一次调用正常工作,但后续调用会引发空引用异常。您可以像其他一些示例一样创建自己的扩展方法来返回任务,但是此方法不适用于 SetupSequence,并且必须使用 Returns 而不是 ReturnsAsync。此外,必须创建一个扩展方法来处理返回结果,这违背了使用 Moq 的初衷。在任何情况下,任何返回类型为 Task 且已将委托传递给 Returns 或 ReturnsAsync 的方法在通过 SetupSequence 设置时在第二次调用时始终会失败。
然而,这种方法有两种有趣的替代方法,只需要最少的额外代码。第一个选项是识别 Mock 对象的 Setup 和 SetupAsync 遵循 Fluent Api 设计模式。这意味着,从技术上讲,Setup、SetupAsync、Returns 和 ReturnsAsync 实际上都返回一个“Builder”对象。我所说的 Builder 类型对象是流畅的 api 样式对象,例如 QueryBuilder、StringBuilder、ModelBuilder 和 IServiceCollection/IServiceProvider。这样做的实际结果是我们可以轻松做到这一点:
var queue = new List<int>(){0,1,2,3};
var setup = mock.SetupSequence(m => m.BarAsync());
foreach(var item in queue)
{
setup.ReturnsAsync(item);
}
这种方法允许我们同时使用SetupSequence和ReturnsAsync,我认为这遵循更直观的设计模式。
第二种方法是认识到 Returns 能够接受返回任务的委托,并且安装程序将始终返回相同的内容。这意味着如果我们要为 Queue 创建一个扩展方法,如下所示:
public static class EMs
{
public static async Task<T> DequeueAsync<T>(this Queue<T> queue)
{
return queue.Dequeue();
}
}
然后我们可以简单地写:
var queue = new Queue<int>(new []{0,1,2,3});
mock.Setup(m => m.BarAsync()).Returns(queue.DequeueAsync);
或者可以使用 Microsoft.VisualStudio.Threading 中的 AsyncQueue 类,这将允许我们执行此操作:
var queue = new AsyncQueue<int>(new []{0,1,2,3});
mock.Setup(m => m.BarAsync()).Returns(queue.DequeueAsync);
导致这一切的主要问题是,当到达设置序列末尾时,该方法被视为未设置。为了避免这种情况,如果在到达序列末尾后返回结果,您还应该调用标准设置。
我整理了一个关于此功能的相当全面的小提琴,其中包含您在做错事情时可能遇到的错误的示例,以及正确做事的几种不同方法的示例。 https://dotnetfiddle.net/KbJlxb
您可以使用临时变量来跟踪该方法被调用的次数。
示例:
public interface ITest
{ Int32 GetNumber(); }
static class Program
{
static void Main()
{
var a = new Mock<ITest>();
var f = 0;
a.Setup(x => x.GetNumber()).Returns(() => f++ == 0 ? 0 : 1);
Debug.Assert(a.Object.GetNumber() == 0);
for (var i = 0; i<100; i++)
Debug.Assert(a.Object.GetNumber() == 1);
}
}
只需设置一个扩展方法,例如:
public static T Denqueue<T>(this Queue<T> queue)
{
var item = queue.Dequeue();
queue.Enqueue(item);
return item;
}
然后设置返回:
var queue = new Queue<int>(new []{0, 1, 1, 1});
mock.Setup(m => m.GetNumber).Returns(queue.Denqueue);
Moq 使用 builder 模式,设置模拟的行为。 您可以,但不必使用流畅的界面。 记住这一点,我这样解决了这个问题:
var sequence = myMock
.SetupSequence(""the method I want to set up"");
foreach (var item in stuffToReturn)
{
sequence = sequence.Returns(item);
}