简体   繁体   中英

Moq SetupSequence with multiple setup assertions

I am writing some tests where the class I'm testing depends on HttpClient . To mock that I am mocking a HttpMessageHandler and pass that to the HttpClient constructor.

To accomplish this I have a base class:

public class HttpTestBase
{
    protected static readonly string BaseAddress = "https://test.com";

    protected readonly HttpClient _httpClient;
    protected readonly Mock<HttpMessageHandler> _httpMessageHandlerMock;

    public HttpTestBase()
    {
        _httpMessageHandlerMock = new Mock<HttpMessageHandler>();
        _httpClient = new HttpClient(_httpMessageHandlerMock.Object);
        _httpClient.BaseAddress = new Uri(BaseAddress);
    }

    protected void MockHttpResponse(HttpResponseMessage message, string expectedPath, HttpMethod expectedMethod)
    {
        _httpMessageHandlerMock
          .Protected()
          .Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.Is<HttpRequestMessage>(request => AssertRequestParameters(expectedPath, expectedMethod, request)),
            ItExpr.IsAny<CancellationToken>())
          .ReturnsAsync(message);
    }

    private bool AssertRequestParameters(string expectedPath, HttpMethod expectedMethod, HttpRequestMessage request)
    {
       // Throw an exception if the method or path does not match what is expected.
    }
}

And then a test looks as follows, in a test class inheriting this test base:

[Fact]
public async Task GetAvailableLicenseCount()
{
    // Arrange
    var licenses = new JsonObject
    {
        ["total_seats_consumed"] = 4500,
        ["total_seats_purchased"] = 5000
    };

    MockHttpResponse(
        new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(licenses.ToJsonString()) },
        expectedPath: "/enterprises/enterprise/consumed-licenses",
        expectedMethod: HttpMethod.Get
    );

    // Act
    var result = await sut.GetAvailableLicenseCount();

    // Assert
    result.Should().Be(500);
}

This works well when a method only performs a single call. Some methods perform multiple calls in a sequence. I have solved that partially, but I would still like to have the assertion in the setup (eg when I call AssertRequestParameters ).

To support multiple calls in a chain, I instead did this in the base class:

protected void AddHttpMockResponse(HttpResponseMessage message, string expectedPath, HttpMethod expectedMethod)
{
    _responseMocks.Enqueue(new HttpMock
    {
        Response = message,
        Assertion = new Assertion { ExpectedPath = expectedPath, ExpectedMethod = expectedMethod }
    });
}

protected void MockHttpResponses()
{
    _httpMessageHandlerMock
        .Protected()
        .Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.IsAny<HttpRequestMessage>(),
            ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(() => _responseMocks.Dequeue().Response);
}

As you can see I have removed the assertion in the setup, because it seems that the assertions collide and I get the wrong return value from the mock.

To use this, I refactored the test as follows:

[Fact]
public async Task GetAvailableLicenseCount()
{
    // Arrange
    var licenses = new JsonObject
    {
        ["total_seats_consumed"] = 4500,
        ["total_seats_purchased"] = 5000
    };

    AddHttpMockResponse(
        new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(licenses.ToJsonString()) },
        expectedPath: "/enterprises/enterprise/consumed-licenses",
        expectedMethod: HttpMethod.Get
    );

    MockHttpResponses();

    // Act
    var result = await sut.GetAvailableLicenseCount();

    // Assert
    result.Should().Be(500);
}

This also works well for multiple responses from the same mock.

However, I would like to be able to mock multiple responses where the response is determined by what I use in the Setup of the mock. The reason I want to do this is that I cannot use Verify on HttpMessageHandler as the method is not accessible to me .

Is it possible to achieve a dynamic return value based on what is done in the setup of the mock?

If I understand your question correctly you'll want something like this:

I recently had to do a similar thing and found the below article very helpful. Check the 'Mock HttpMessageHandler Using Moq' section. It shows you how to return a HttpResponseMessage object you create, which is what I think you might want.

https://code-maze.com/csharp-mock-httpclient-with-unit-tests/

Field on your class

private readonly Mock<HttpMessageHandler> _httpMessageHandlerStub;

In your setup (constructor or a separate method)

_httpMessageHandlerStub = new Mock<HttpMessageHandler>();

var httpClient = new HttpClient(_httpMessageHandlerStub.Object);

_sut = new SomeService(
    httpClient,
    anotherDependency
)

And then you can configure the below in each test

[Fact]
public async Task GetListAsync_Successfully_Uses_Api_Key()
{
    var unauthorisedResponseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized)
    {
        Content = new StringContent($"'{_apiKeyHeaderName}' header not found or API key is incorrect")
    };

    var successResponseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
    {
        Content = new StringContent(JsonConvert.SerializeObject(new List<SomeDto>()))
    };

    // Return 200 if api key header is present and the value is correct.
    _httpMessageHandlerStub.Protected()
        .Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.Is<HttpRequestMessage>(x => x.Method == HttpMethod.Get && x.Headers.Any(h => h.Key == _apiKeyHeaderName && h.Value.FirstOrDefault() == _apiKeyHeaderValue)),
            ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(successResponseMessage);

    // Return 401 is api key header is not present or api key value is incorrect.
    _httpMessageHandlerStub.Protected()
        .Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.Is<HttpRequestMessage>(x => x.Method == HttpMethod.Get && !x.Headers.Any(h => h.Key == _apiKeyHeaderName && h.Value.FirstOrDefault() == _apiKeyHeaderValue)),
            ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(unauthorisedResponseMessage);

    var result = _sut.GetListAsync();

    // Assert stuff...
}

Hope that's helpful.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM