ChatGPT解决这个技术问题 Extra ChatGPT

Using Moq to mock an asynchronous method for a unit test

I am testing a method for a service that makes a Web API call. Using a normal HttpClient works fine for unit tests if I also run the web service (located in another project in the solution) locally.

However when I check in my changes the build server won't have access to the web service so the tests will fail.

I've devised a way around this for my unit tests by creating an IHttpClient interface and implementing a version that I use in my application. For unit tests, I make a mocked version complete with a mocked asynchronous post method. Here's where I have run into problems. I want to return an OK HttpStatusResult for this particular test. For another similar test I will be returning a bad result.

The test will run but will never complete. It hangs at the await. I am new to asynchronous programming, delegates, and Moq itself and I've been searching SO and google for a while learning new things but I still can't seem to get past this problem.

Here is the method I am trying to test:

public async Task<bool> QueueNotificationAsync(IHttpClient client, Email email)
{
    // do stuff
    try
    {
        // The test hangs here, never returning
        HttpResponseMessage response = await client.PostAsync(uri, content);

        // more logic here
    }
    // more stuff
}

Here's my unit test method:

[TestMethod]
public async Task QueueNotificationAsync_Completes_With_ValidEmail()
{
    Email email = new Email()
    {
        FromAddress = "bob@example.com",
        ToAddress = "bill@example.com",
        CCAddress = "brian@example.com",
        BCCAddress = "ben@example.com",
        Subject = "Hello",
        Body = "Hello World."
    };
    var mockClient = new Mock<IHttpClient>();
    mockClient.Setup(c => c.PostAsync(
        It.IsAny<Uri>(),
        It.IsAny<HttpContent>()
        )).Returns(() => new Task<HttpResponseMessage>(() => new HttpResponseMessage(System.Net.HttpStatusCode.OK)));

    bool result = await _notificationRequestService.QueueNotificationAsync(mockClient.Object, email);

    Assert.IsTrue(result, "Queue failed.");
}

What am I doing wrong?

Thank you for your help.


J
Jon Skeet

You're creating a task but never starting it, so it's never completing. However, don't just start the task - instead, change to using Task.FromResult<TResult> which will give you a task which has already completed:

...
.Returns(Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK)));

Note that you won't be testing the actual asynchrony this way - if you want to do that, you need to do a bit more work to create a Task<T> that you can control in a more fine-grained manner... but that's something for another day.

You might also want to consider using a fake for IHttpClient rather than mocking everything - it really depends on how often you need it.


Thank you very much. That worked great. I figured it was probably something simple that I wasn't understanding.
Re: Fake IHttpClient, I considered that but I needed to be able to return different HttpStatusCodes for different tests based on the expected behavior coming back from the web API, and this seemed to give me more control.
@mvanella: Yes, so you'd create a fake which can return whatever you want it to. Just something to think about.
For anyone who finds this now, Moq 4.2 has an extension called ReturnsAysnc, which does exactly this.
@legacybass I can't find a link to any documentation for it, even though the API docs says they're built against v4.2.1312.1622 which was released almost exactly a year ago. See this commit which was made a few days before that release. As to why the API documents aren't update...
D
DineshNS

Recommend @Stuart Grassie's answer above.

var moqCredentialMananger = new Mock<ICredentialManager>();
moqCredentialMananger
                    .Setup(x => x.GetCredentialsAsync(It.IsAny<string>()))
                    .ReturnsAsync(new Credentials() { .. .. .. });

b
bside

With Mock.Of<...>(...) for async method you can use Task.FromResult(...):

var client = Mock.Of<IHttpClient>(c => 
    c.PostAsync(It.IsAny<Uri>(), It.IsAny<HttpContent>()) == Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))
);

K
Kurt Van den Branden

Try using ReturnsAsync. In asynchronous methods it works, I believe the basis to solve your problem should be similar.

_mocker.GetMock<IMyRepository>()
     .Setup(x => x.GetAll())
     .ReturnsAsync(_myFakeListRepository.GetAll());