【问题标题】:Nunit async test exception assertionNunit 异步测试异常断言
【发布时间】:2013-03-16 02:22:52
【问题描述】:

[编辑(2020 年 5 月)] - 据报道,此问题已在 NUnit 的较新版本中得到解决。请参阅Nunit.ThrowsAsync。 (参考this answer,感谢@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. [测试] 公共无效 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. 我不想只测试抛出的异常。我想断言响应的状态代码。这样做的方法是什么?

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

【问题讨论】:

  • 您应该从您的公共 Task Get(string domainUserName) 添加更多代码,因为您的所有测试都是针对 domainUser = "foo" 并且您显示的唯一错误是针对空 domainUser(或空)
  • @JleruOHeP - 谢谢 - 有点忘乎所以。已编辑代码。
  • 修改后所有的测试用例还是一样的吗?而且情况1仍然没有抛出任何异常?
  • @JleruOHeP - 是的,测试失败,没有抛出异常。
  • +1 个非常好的问题,帮助我发现了我的问题。

标签: c# nunit async-await


【解决方案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

【讨论】:

  • 使用 NUnit 3.12.0 似乎不需要第 2 步,因此以下操作仍按预期工作:var ex = Assert.ThrowsAsync&lt;ArgumentException&gt;(() =&gt; MethodThatThrows()); 我在测试中使用这种方法。这也正如@james-ross 在stackoverflow.com/a/40030988/411428 的回答中所建议的那样(见上文)。
【解决方案2】:

由于async void,您遇到了问题。

特别是:

  1. async () =&gt; 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,但在这种情况下,测试运行程序会等待完成,因此会看到异常。

根据this bug report,在 NUnit 的下一个版本中会对此进行修复。同时,您可以构建自己的ThrowsAsync 方法;一个example for xUnit is here

【讨论】:

  • 谢谢 - 我怀疑有错误,很高兴它得到确认。我将使用ThrowsAsync 方法,看起来比我现在的干净得多。
  • 您的方法效果很好,我不得不扩展您的 ThrowsAsync 想法以添加断言功能,但这并不太难。我已经用我现在所拥有的内容更新了下面的答案。再次感谢。
  • 自 2.6.3 以来已修复错误
  • 我可以确认以下代码在NUnit 3.11.0中有效:Assert.That(async() =&gt; await something.AsyncOperation(...)), Throws.Exception);
【解决方案3】:

我有一个与场景 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()));
}    

【讨论】:

  • 因为这是同步操作,你必须使用async/await
【解决方案4】:

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

【讨论】:

  • 这个答案应该在顶部,这样人们就不会浪费时间尝试所有定制的解决方案。答案已经立即内置到 NUnit 中
  • 唷,我很高兴我没有停止向下滚动 :)
  • 当使用 Assert.ThrowsAsync 我不认为你的测试需要是异步的(在这种情况下)。就让它作废吧。
  • 任何人都可以验证在这种情况下不需要async 吗?我可能刚刚拒绝了对这篇文章的有效编辑。
  • 我可以,但这是我的编辑 :) async void 给我一个“异步测试方法必须具有非 void 返回类型”错误。删除异步它将按预期工作。
【解决方案5】:

This blog 讲了和我类似的问题。

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

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

【讨论】:

    【解决方案6】:

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

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

    Link to MSDN

    【讨论】:

    • 是的,确实如此。我不想查看AggregateException 来检查是否抛出了HttpResponseException,但看起来没有选项?
    • 我认为没有办法解决 AggregateException,但我认为这种方式还不错。
    猜你喜欢
    • 2012-08-15
    • 1970-01-01
    • 2019-11-21
    • 1970-01-01
    • 2014-08-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-27
    相关资源
    最近更新 更多