45

[编辑(2020 年 5 月)] - 据报道,此问题已在 NUnit 的较新版本中得到解决。请参阅Nunit.ThrowsAsync。(参考这个答案,谢谢@James-Ross)


我有一个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
  1. [测试] public void someTest() { var mockUserRepository = new Mock(); mockUserRepository.Setup(x => x.GetByUserName(It.IsAny())).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.)
[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
[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");
}

结果 测试通过

问题 -

  1. 为什么 Assert.Throws 不处理 HttpResponseException,而 ExpectedException 处理?
  2. 我不想只测试抛出的异常。我想断言响应的状态代码。这样做的方法是什么?

对这些行为及其原因的任何比较都会很棒!

4

6 回答 6

57

您看到的问题是由于async void.

尤其是:

  1. async () => await userController.Get("foo")被转换为TestDelegate, 返回void, 所以你的 lambda 表达式被视为async void. 所以测试运行器将开始执行 lambda,但不会等待它完成。lambda 在Get完成之前返回(因为它是async),并且测试运行器看到它没有异常地返回。

  2. Wait将任何异常包装在AggregateException.

  3. 同样,asynclambda 被视为async void,因此测试运行程序不会等待其完成。

  4. 我建议你做 thisasync Task而不是async void,但在这种情况下,测试运行器会等待完成,因此会看到异常。

根据这个错误报告,在 NUnit 的下一个版本中会对此进行修复。同时,您可以构建自己的ThrowsAsync方法;xUnit的示例在这里

于 2013-03-26T12:13:18.887 回答
50

我不确定它是何时添加的,但 Nunit 的当前版本(撰写本文时为 3.4.1)包含 ThrowsAsync 方法

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

例子:

[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 exception = Assert.ThrowsAsync<HttpResponseException>(() => userController.Get("foo"));

    Assert.That(exception.Response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}
于 2016-10-13T21:05:18.680 回答
12

该博客讨论了与我类似的问题。

我遵循了那里提出的建议,并进行了这样的测试-

    [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);
    }
于 2013-03-26T12:09:54.597 回答
3

如果您等待任务,则抛出的异常将聚合到 AggregateException。您可以检查 AggregateException 的内部异常。这可能是您的案例 2 不起作用的原因。

由在任务内运行的用户代码引发的未处理异常会传播回加入线程,但本主题后面描述的某些场景除外。当您使用静态或实例 Task.Wait 或 Task.Wait 方法之一时会传播异常,并且您通过将调用包含在 try-catch 语句中来处理它们。如果一个任务是附加子任务的父任务,或者如果您正在等待多个任务,则可能会引发多个异常。要将所有异常传播回调用线程,Task 基础结构将它们包装在 AggregateException 实例中。AggregateException 有一个 InnerExceptions 属性,可以枚举该属性以检查所有引发的原始异常,并单独处理(或不处理)每个异常。即使只抛出一个异常,

链接到 MSDN

于 2013-03-26T10:53:42.060 回答
2

我有一个与场景 3 测试用例类似的问题,由于以下结果而失败

Expected: <UserDefineException>
But was:  null

通过使用 Assert.ThrowAsync<> 问题得到解决

我的 Web API 操作方法和单元测试用例方法如下

public async Task<IHttpActionResult> ActionMethod(RequestModel requestModel)
{
   throw UserDefineException();
}


[Test]
public void Test_Contrller_Method()
{
   Assert.ThrowsAsync<UserDefineException>(() => _controller.ActionMethod(new RequestModel()));
}    
于 2018-05-25T06:22:18.350 回答
1

这是文档中的一个示例:

var ex = Assert.ThrowsAsync<ArgumentException>(async () => await MethodThatThrows());
  1. 利用ThrowsAsync
  2. 使用async/await

https://docs.nunit.org/articles/nunit/writing-tests/assertions/classic-assertions/Assert.ThrowsAsync.html

于 2020-08-22T02:43:24.203 回答