Nunit异步测试异常断言

52

[编辑(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. [Test] 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. 我不仅想测试异常是否抛出,还想断言响应的状态码。怎么做?

欢迎对这些行为及其原因进行比较!


你修改后所有的测试用例还是一样吗?而且第一个测试用例仍然没有抛出任何异常? - JleruOHeP
@JleruOHeP - 是的,测试失败了,没有抛出任何异常。 - Srikanth Venugopalan
6个回答

62

我不确定它是何时添加的,但目前版本的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));
}

11
答案应该置于顶部,以便人们不会浪费时间尝试所有定制的解决方案。答案已经即时集成到 NUnit 中。 - Shahzad Qureshi
2
哎呀,我很高兴我没有停止滚动 :) - Caio Campos
1
有人能验证在这种情况下不需要使用 async 吗?我可能刚刚拒绝了一个有效的对此帖子的编辑。 - Munim Munna
我可以,但那是我的修改 :) 使用async void会给我带来“异步测试方法必须有非void返回类型”错误。去掉async后,它将按预期工作。 - AndrewK
鉴于即使在2018年这个问题仍然很受欢迎,我已经在问题本身中包含了对此答案的引用。谢谢。 - Srikanth Venugopalan
显示剩余3条评论

60

你看到的问题是由于使用了 async void

具体来说:

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

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

  3. 同样,这个 async 的 lambda 被视作 async void,因此测试运行器不会等待其完成。

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

根据这个 bug 报告,NUnit 的下一个版本中会有修复此问题的方法。在此期间,你可以构建自己的 ThrowsAsync 方法;这里有一个 xUnit 的示例


谢谢 - 我怀疑有个 bug,很高兴现在得到了确认。我会使用 ThrowsAsync 方法,看起来比我现在的代码更简洁。 - Srikanth Venugopalan
你的方法很好用,我需要在你的ThrowsAsync想法上做一些扩展,以添加断言功能,但这并不太困难。我已经更新了我的答案,以下是我目前的代码。再次感谢。 - Srikanth Venugopalan
2
自2.6.3版本以来,错误已经被修复。 - DalSoft
1
我可以确认以下代码在 NUnit 3.11.0 中有效:Assert.That(async() => await something.AsyncOperation(...)), Throws.Exception); - Sander Aernouts

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);
    }

5

2
使用 NUnit 3.12.0,似乎不需要步骤2,因此以下内容仍按预期工作:var ex = Assert.ThrowsAsync<ArgumentException>(() => MethodThatThrows()); 我在我的测试中使用了这种方法。正如 @james-ross 在 https://dev59.com/t2Uo5IYBdhLWcg3w9jV4#40030988 中建议的那样(请参见上文)。 - Manfred

3
如果您等待一个任务,那么抛出的异常会被聚合到AggregateException中。您可以检查AggregateException的内部异常。这可能是你的情况2不起作用的原因。
除了本主题后面描述的某些情况外,在任务内运行的用户代码抛出的未处理异常将向上传播到加入线程。当您使用静态或实例Task.Wait或Task.Wait方法并通过将调用包含在try-catch语句中来处理它们时,异常将被传播。如果一个任务是附加子任务的父任务,或者如果您正在等待多个任务,则可能抛出多个异常。为了将所有异常传播回调用线程,任务基础结构将它们包装在AggregateException实例中。AggregateException有一个InnerExceptions属性,可以枚举以检查所有最初抛出的异常并逐个处理(或不处理)每个异常。即使只抛出一个异常,它也仍然被包装在AggregateException中。 MSDN链接

是的,确实如此。我不想查看AggregateException以检查是否抛出了HttpResponseException,但看起来没有其他选项? - Srikanth Venugopalan
我认为没有绕过查看AggregateException的方法,但我认为这种方式并不太糟糕。 - roqz

2

我有一个和你在第三种情况中类似的问题,在测试案例中失败了,原因如下:

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()));
}    

由于这是同步操作,必须使用 async/await - Jaider

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接