Использование Moq для макетирования асинхронного метода для модульного теста

180

Я тестирую метод для службы, которая выполняет веб- APIвызов. Использование обычного нормально HttpClientработает для модульных тестов, если я также запускаю веб-сервис (расположенный в другом проекте в решении) локально.

Однако, когда я регистрирую свои изменения, у сервера сборки не будет доступа к веб-службе, поэтому тесты не пройдут.

Я разработал способ обойти это для своих модульных тестов, создав IHttpClientинтерфейс и внедрив версию, которую я использую в своем приложении. Для модульных тестов я делаю макетную версию в комплекте с симулированным асинхронным пост-методом. Вот где я столкнулся с проблемами. Я хочу вернуть ОК HttpStatusResultдля этого конкретного теста. Для другого подобного теста я буду возвращать плохой результат.

Тест будет запущен, но никогда не будет завершен. Он висит в ожидании. Я новичок в асинхронном программировании, делегатах и ​​самом Moq, и я некоторое время искал SO и Google, изучая новые вещи, но я все еще не могу решить эту проблему.

Вот метод, который я пытаюсь проверить:

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
}

Вот мой метод модульного тестирования:

[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.");
}

Что я делаю не так?

Спасибо за помощь.

mvanella
источник

Ответы:

351

Вы создаете задачу, но никогда не запускаете ее, поэтому она никогда не завершается. Однако не просто запустите задачу - вместо этого перейдите на использование, Task.FromResult<TResult>которое даст вам задачу, которая уже выполнена:

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

Обратите внимание, что вы не будете тестировать фактическую асинхронность таким образом - если вы хотите это сделать, вам нужно проделать немного больше работы, чтобы создать Task<T>более детальный способ управления, но это кое-что для другой день

Возможно, вы захотите использовать фальшивку, IHttpClientа не издеваться над всем - это действительно зависит от того, как часто вам это нужно.

Джон Скит
источник
2
Большое спасибо. Это сработало отлично. Я подумал, что, возможно, это было что-то простое, чего я не понимал.
Мванелла
2
Re: Поддельный IHttpClient, я считал это, но мне нужно было иметь возможность возвращать разные HttpStatusCodes для разных тестов на основе ожидаемого поведения, возвращающегося из веб-API, и это, казалось, дало мне больше контроля.
Мванелла
3
@mvanella: Да, так что вы создали бы подделку, которая может вернуть все, что вы хотите. Просто кое что для раздумий.
Джон Скит
134
Для тех, кто находит это сейчас, в Moq 4.2 есть расширение с названием ReturnsAysnc, которое делает именно это.
Стюарт Грасси
3
@legacybass Я не могу найти ссылку на какую-либо документацию для него, хотя в документации API написано, что они созданы для v4.2.1312.1622, который был выпущен почти ровно год назад. Смотрите этот коммит, который был сделан за несколько дней до этого релиза. Относительно того, почему документы API не обновляются ...
Стюарт Грасси
17

Порекомендуйте ответ @Stuart Grassie выше.

var moqCredentialMananger = new Mock<ICredentialManager>();
moqCredentialMananger
                    .Setup(x => x.GetCredentialsAsync(It.IsAny<string>()))
                    .ReturnsAsync(new Credentials() { .. .. .. });
DineshNS
источник
1

С Mock.Of<...>(...)для asyncметода вы можете использовать Task.FromResult(...):

var client = Mock.Of<IHttpClient>(c => 
    c.PostAsync(It.IsAny<Uri>(), It.IsAny<HttpContent>()) == Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))
);
б сторона
источник