Подтвердить что ты не робот

Утверждение исключения асинхронного теста Nunit

У меня есть контроллер UserController с этим действием

// GET /blah
public Task<User> Get(string domainUserName)
{
        if (string.IsNullOrEmpty(domainUserName))
        {
            throw new ArgumentException("No username specified.");
        }

        return Task.Factory.StartNew(
            () =>
                {
                    var user = userRepository.GetByUserName(domainUserName);
                    if (user != null)
                    {
                        return user;
                    }

                    throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.NotFound, string.Format("{0} - username does not exist", domainUserName)));
                });
}

Я пытаюсь написать тест для случая, когда я выбрал исключение 404.

Вот что я пробовал, с выходом -

1)

[Test]
public void someTest()
{
        var mockUserRepository = new Mock<IUserRepository>();
        mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));
    var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

    Assert.That(async () => await userController.Get("foo"), Throws.InstanceOf<HttpResponseException>());
}

Результат Не удалось выполнить проверку

  Expected: instance of <System.Web.Http.HttpResponseException>
  But was:  no exception thrown

2)

[Test]
public void someTest()
{
        var mockUserRepository = new Mock<IUserRepository>();
        mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));
    var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

    var httpResponseException = Assert.Throws<HttpResponseException>(() => userController.Get("foo").Wait());
    Assert.That(httpResponseException.Response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}

Результат Не удалось выполнить тест

  Expected: <System.Web.Http.HttpResponseException>
  But was:  <System.AggregateException> (One or more errors occurred.)

3)

[Test]
public void someTest()
{
        var mockUserRepository = new Mock<IUserRepository>();
        mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));
    var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

    var httpResponseException = Assert.Throws<HttpResponseException>(async () => await userController.Get("foo"));
    Assert.That(httpResponseException.Response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}

Результат Не удалось выполнить проверку

  Expected: <System.Web.Http.HttpResponseException>
  But was:  null

4)

[Test]
[ExpectedException(typeof(HttpResponseException))]
public async void ShouldThrow404WhenNotFound()
{            var mockUserRepository = new Mock<IUserRepository>();
        mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));

    var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

    var task = await userController.Get("foo");
}

Результат Тест проходит

Вопросы -

  • Почему Assert.Throws не обрабатывает исключение HttpResponseException, когда ExpectedException делает?
  • Я не хочу просто проверять, что это исключение. Я хочу заявить в Кодексе статуса ответа. Каким образом это сделать?

Любое сравнение этого поведения и его причин (ов) было бы замечательным!

4b9b3361

Ответ 1

Вы видите проблемы из-за async void.

В частности:

1) async () => await userController.Get("foo") преобразуется в TestDelegate, который возвращает void, поэтому ваше лямбда-выражение рассматривается как async void. Таким образом, тестовый бегун начнет выполнение лямбды, но не дожидаясь его завершения. Лямбда возвращается до завершения Get (потому что она async), а тестовый бегун видит, что он возвращается без исключения.

2) Wait завершает любые исключения в AggregateException.

3) Опять же, async lambda рассматривается как async void, поэтому тестовый бегун не ждет его завершения.

4) Я рекомендую вам сделать это async Task, а не async void, но в этом случае тестовый бегун ждет завершения и, следовательно, видит исключение.

В соответствии с этот отчет об ошибках, есть исправление для этого в следующей сборке NUnit. Тем временем вы можете создать свой собственный метод ThrowsAsync; пример для xUnit здесь.

Ответ 2

Я не уверен, когда он был добавлен, но текущая версия Nunit (3.4.1 на момент написания) включает метод ThrowsAsync

см. https://github.com/nunit/docs/wiki/Assert.ThrowsAsync

Я не тестировал этот пример конкретно, но он должен работать следующим образом:

[Test]
public async void ShouldThrow404WhenNotFound()
{
    var mockUserRepository = new Mock<IUserRepository>();
    mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));
    var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

    var exception = Assert.ThrowsAsync<HttpResponseException>(() => userController.Get("foo"));

    Assert.That(exception.Response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}

Ответ 3

Этот блог рассказывает о проблемах, похожих на мои.

Я следовал предложенной там рекомендации и имел такой тест:

    [Test]
    public void ShouldThrow404WhenNotFound()
    {
        var mockUserRepository = new Mock<IUserRepository>();
        mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));
        var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

        var aggregateException = Assert.Throws<AggregateException>(() => userController.Get("foo").Wait());
        var httpResponseException = aggregateException.InnerExceptions
            .FirstOrDefault(x => x.GetType() == typeof(HttpResponseException)) as HttpResponseException;

        Assert.That(httpResponseException, Is.Not.Null);
        Assert.That(httpResponseException.Response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
    }

Я не очень доволен этим, но это работает.

РЕДАКТИРОВАТЬ 1

Вдохновленный @StephenCleary, я добавил статический вспомогательный класс, который делает утверждения, которые я ищу. Это выглядит так:

public static class AssertEx
{
    public static async Task ThrowsAsync<TException>(Func<Task> func) where TException : class
    {
        await ThrowsAsync<TException>(func, exception => { });
    } 

    public static async Task ThrowsAsync<TException>(Func<Task> func, Action<TException> action) where TException : class
    {
        var exception = default(TException);
        var expected = typeof(TException);
        Type actual = null;
        try
        {
            await func();
        }
        catch (Exception e)
        {
            exception = e as TException;
            actual = e.GetType();
        }

        Assert.AreEqual(expected, actual);
        action(exception);
    }
}

Теперь у меня есть тест вроде -

    [Test]
    public async void ShouldThrow404WhenNotFound()
    {
        var mockUserRepository = new Mock<IUserRepository>();
        mockUserRepository.Setup(x => x.GetByUserName(It.IsAny<string>())).Returns(default(User));
        var userController = new UserController(mockUserRepository.Object) { Request = new HttpRequestMessage() };

        Action<HttpResponseException> asserts = exception => Assert.That(exception.Response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
        await AssertEx.ThrowsAsync(() => userController.Get("foo"), asserts);
    }

Ответ 4

Если вы ожидаете задачу, то Исключения, которые были выбраны, агрегируются в AggregateException. Вы можете проверить внутренние исключения из AggregateException. Это может быть причиной того, что ваш случай 2 не работает.

Необработанные исключения, которые вызывают код пользователя, который запущен внутри задачи, передаются обратно в поток объединения, за исключением определенных сценариев, описанных ниже в этом разделе. Исключения распространяются при использовании одного из статических методов или экземпляров Task.Wait или Task.Wait, и вы обрабатываете их, закрывая вызов в заявлении try-catch. Если задание является родительским элементом связанных дочерних задач или если вы ожидаете нескольких задач, может быть выбрано несколько исключений. Чтобы распространить все исключения обратно на вызывающий поток, инфраструктура Task обертывает их в экземпляр AggregateException. Свойство AggregateException имеет свойство InnerExceptions, которое можно перечислить, чтобы просмотреть все исходные исключения, которые были выбраны, и обрабатывать (или не обрабатывать) каждый отдельно. Даже если выбрано только одно исключение, оно все равно завернуто в исключение AggregateException.

Ссылка на MSDN