使用 Moq 计算递归方法被调用的次数
Count number of times a recursive method is called using Moq
我有这两种方法,SendMessage
是递归且不可覆盖的,SendMessageUsingHttpClient
是 virtual
所以我可以在我的测试中模拟任何 return class.
public HttpResponseMessage SendMessage(HttpRequestMessage httpRequestMessage, HttpStatusCode? httpStatusCode = null)
{
// not relevant
GetRandomTokenAndAddInMessageHeader(httpRequestMessage, httpStatusCode);
var response = SendMessageUsingHttpClient(httpRequestMessage);
if (response.StatusCode == HttpStatusCode.Unauthorized)
SendMessage(httpRequestMessage, HttpStatusCode.Unauthorized);
return response;
}
public virtual HttpResponseMessage SendMessageUsingHttpClient(HttpRequestMessage httpRequestMessage)
{
return _httpClient.Send(httpRequestMessage, CancellationToken.None);
}
我需要测试这条线if (response.StatusCode == HttpStatusCode.Unauthorized)
,所以这是我的测试方法:
[TestMethod]
public void Send_MustReturn_UnauthorizedMock_Then_OkMock()
{
var mockHttpResponseManager = new Mock<HttpRequestManager>();
var expectedUnauthorizedResponse = new HttpResponseMessage { StatusCode = HttpStatusCode.Unauthorized };
var expectedOkResponse = new HttpResponseMessage { StatusCode = HttpStatusCode.OK };
mockHttpResponseManager.SetupSequence(m => m.SendMessageUsingHttpClient(It.IsAny<HttpRequestMessage>()))
.Returns(expectedUnauthorizedResponse)
.Returns(expectedOkResponse);
mockHttpResponseManager.Object.SendMessage(new HttpRequestMessage());
mockHttpResponseManager.Verify(v => v.SendMessage(It.IsAny<HttpRequestMessage>(), It.IsAny<HttpStatusCode?>()), Times.Exactly(2));
}
如果我 运行 这个测试,因为 SendMessage
不是 virtual
,它会抛出以下异常:
Unsupported expression: v => v.SendMessage(It.IsAny(), It.IsAny<HttpStatusCode?>())
Non-overridable members (here: HttpRequestManager.SendMessage) may not be used in setup / verification expressions.
如果我将 SendMessage
变为 virtual
,Moq 将不会计算第二次(递归)调用,只计算 Moq 在我的测试中调用的调用 class。
Expected invocation on the mock exactly 2 times, but was 1 times:
v => v.SendMessage(It.IsAny(), It.IsAny<HttpStatusCode?>())
Performed invocations:
MockHttpRequestManager:1 (v):
HttpRequestManager.SendMessage(Method: GET, RequestUri: '', Version: 1.1, Content: , Headers: {}, null)
在单元测试期间,我们应该模拟依赖项。我们想要注入一个对象,它的行为就像我们想要的那样。因此,与其模拟被测系统 (SUT),不如像 devNull 所述那样,重点模拟依赖关系。
为了简单起见,让我们忘记 HttpClient 并有一个更简单的依赖关系:
public interface IDependency
{
int Calculate(int input);
}
public class Dependency: IDependency
{
public int Calculate(int input) => input + 1;
}
现在,让我们也简化您的 SUT:
public class SUT
{
public const int MagicNumber = 42;
private readonly IDependency dependency;
public SUT(IDependency dependency)
{
this.dependency = dependency;
}
public int PublicApi(int input)
{
var response = PrivateApi(input);
if (response == MagicNumber)
PublicApi(input);
return response;
}
private int PrivateApi(int input)
{
return dependency.Calculate(input);
}
}
- 因此,我们通过构造函数接收到一个
IDependency
实例。
- 我们在
PrivateApi
中调用它的 Calculate
方法
PrivateApi
通过 PublicApi
调用
在您的测试用例中,您真正需要的是模拟 IDependency
。它的行为应该引起 PublicApi
.
的递归调用
您可以对模拟实例进行调用计数评估。 (所以,你的模拟也可以充当间谍)。
[Fact]
public void GivenADependency_FirstReturnsMagicNumber_AndSecondReturnsDifferentValue_WhenICallPublicApi_ThenItCallsTheDependencyTwice()
{
//Arrange
var dependencyMock = new Mock<IDependency>();
dependencyMock.SetupSequence(dep => dep.Calculate(It.IsAny<int>()))
.Returns(SUT.MagicNumber)
.Returns(SUT.MagicNumber - 1);
var sut = new SUT(dependencyMock.Object);
//Act
sut.PublicApi(1);
//Arrange
dependencyMock.Verify(dep => dep.Calculate(It.IsAny<int>()), Times.Exactly(2));
}
为了完整起见,我在这里分享一些关于测试替身的术语
- Dummy: returns伪数据
的简单代码
- Fake:一个可以走捷径的可行替代方案
- 存根:具有预定义数据的自定义逻辑
- Mock:带有期望的自定义逻辑(交互式存根)
- Shim:运行 时的自定义逻辑(用委托替换静态)
- Spy: 拦截器来记录调用
我有这两种方法,SendMessage
是递归且不可覆盖的,SendMessageUsingHttpClient
是 virtual
所以我可以在我的测试中模拟任何 return class.
public HttpResponseMessage SendMessage(HttpRequestMessage httpRequestMessage, HttpStatusCode? httpStatusCode = null)
{
// not relevant
GetRandomTokenAndAddInMessageHeader(httpRequestMessage, httpStatusCode);
var response = SendMessageUsingHttpClient(httpRequestMessage);
if (response.StatusCode == HttpStatusCode.Unauthorized)
SendMessage(httpRequestMessage, HttpStatusCode.Unauthorized);
return response;
}
public virtual HttpResponseMessage SendMessageUsingHttpClient(HttpRequestMessage httpRequestMessage)
{
return _httpClient.Send(httpRequestMessage, CancellationToken.None);
}
我需要测试这条线if (response.StatusCode == HttpStatusCode.Unauthorized)
,所以这是我的测试方法:
[TestMethod]
public void Send_MustReturn_UnauthorizedMock_Then_OkMock()
{
var mockHttpResponseManager = new Mock<HttpRequestManager>();
var expectedUnauthorizedResponse = new HttpResponseMessage { StatusCode = HttpStatusCode.Unauthorized };
var expectedOkResponse = new HttpResponseMessage { StatusCode = HttpStatusCode.OK };
mockHttpResponseManager.SetupSequence(m => m.SendMessageUsingHttpClient(It.IsAny<HttpRequestMessage>()))
.Returns(expectedUnauthorizedResponse)
.Returns(expectedOkResponse);
mockHttpResponseManager.Object.SendMessage(new HttpRequestMessage());
mockHttpResponseManager.Verify(v => v.SendMessage(It.IsAny<HttpRequestMessage>(), It.IsAny<HttpStatusCode?>()), Times.Exactly(2));
}
如果我 运行 这个测试,因为 SendMessage
不是 virtual
,它会抛出以下异常:
Unsupported expression: v => v.SendMessage(It.IsAny(), It.IsAny<HttpStatusCode?>()) Non-overridable members (here: HttpRequestManager.SendMessage) may not be used in setup / verification expressions.
如果我将 SendMessage
变为 virtual
,Moq 将不会计算第二次(递归)调用,只计算 Moq 在我的测试中调用的调用 class。
Expected invocation on the mock exactly 2 times, but was 1 times: v => v.SendMessage(It.IsAny(), It.IsAny<HttpStatusCode?>()) Performed invocations: MockHttpRequestManager:1 (v): HttpRequestManager.SendMessage(Method: GET, RequestUri: '', Version: 1.1, Content: , Headers: {}, null)
在单元测试期间,我们应该模拟依赖项。我们想要注入一个对象,它的行为就像我们想要的那样。因此,与其模拟被测系统 (SUT),不如像 devNull 所述那样,重点模拟依赖关系。
为了简单起见,让我们忘记 HttpClient 并有一个更简单的依赖关系:
public interface IDependency
{
int Calculate(int input);
}
public class Dependency: IDependency
{
public int Calculate(int input) => input + 1;
}
现在,让我们也简化您的 SUT:
public class SUT
{
public const int MagicNumber = 42;
private readonly IDependency dependency;
public SUT(IDependency dependency)
{
this.dependency = dependency;
}
public int PublicApi(int input)
{
var response = PrivateApi(input);
if (response == MagicNumber)
PublicApi(input);
return response;
}
private int PrivateApi(int input)
{
return dependency.Calculate(input);
}
}
- 因此,我们通过构造函数接收到一个
IDependency
实例。 - 我们在
PrivateApi
中调用它的 PrivateApi
通过PublicApi
调用
Calculate
方法
在您的测试用例中,您真正需要的是模拟 IDependency
。它的行为应该引起 PublicApi
.
您可以对模拟实例进行调用计数评估。 (所以,你的模拟也可以充当间谍)。
[Fact]
public void GivenADependency_FirstReturnsMagicNumber_AndSecondReturnsDifferentValue_WhenICallPublicApi_ThenItCallsTheDependencyTwice()
{
//Arrange
var dependencyMock = new Mock<IDependency>();
dependencyMock.SetupSequence(dep => dep.Calculate(It.IsAny<int>()))
.Returns(SUT.MagicNumber)
.Returns(SUT.MagicNumber - 1);
var sut = new SUT(dependencyMock.Object);
//Act
sut.PublicApi(1);
//Arrange
dependencyMock.Verify(dep => dep.Calculate(It.IsAny<int>()), Times.Exactly(2));
}
为了完整起见,我在这里分享一些关于测试替身的术语
- Dummy: returns伪数据 的简单代码
- Fake:一个可以走捷径的可行替代方案
- 存根:具有预定义数据的自定义逻辑
- Mock:带有期望的自定义逻辑(交互式存根)
- Shim:运行 时的自定义逻辑(用委托替换静态)
- Spy: 拦截器来记录调用