使用 Moq 计算递归方法被调用的次数

Count number of times a recursive method is called using Moq

我有这两种方法,SendMessage 是递归且不可覆盖的,SendMessageUsingHttpClientvirtual 所以我可以在我的测试中模拟任何 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: 拦截器来记录调用