C# 对预期失败的 async 方法进行单元测试

假设你需要编写单元测试来检查 async Task 方法的某次失败。

如果在进行桌面端开发或服务器端开发,那么 MSTest 能够通过常规的 ExpectedException 属性来支持失败测试:

// 此解决方案并非推荐方案,稍后解释原因
    [TestMethod]
    [ExpectedException(typeof(DivideByZeroException))]
    public async Task Divide_WhenDenominatorIsZero_ThrowsDivideByZero()
    {
        await MyClass.DivideAsync(4, 0);
    }

然而,此解决方案并非最佳,ExpectedException 其实是个糟糕的设计,单元测试调用的所有方法都可能抛出它期待的异常。与其整体检查单元测试,不如检查特定部分的代码是否抛出了异常。

大多数现代单元测试框架以某种形式包含了 Assert.ThrowsAsync<TException>。比如,可以像下面这样使用 xUnit 的 ThrowsAsync

[Fact]
    public async Task Divide_WhenDenominatorIsZero_ThrowsDivideByZero()
    {
        await Assert.ThrowsAsync<DivideByZeroException>(async () =>
        {
            await MyClass.DivideAsync(4, 0);
        });
    }

别忘了等待由 ThrowsAsync 返回的任务!await 会传递所有检测到的断言失败。如果忘了使用 await,并无视了编译器警告,则不论方法行为如何,单元测试都会标记成功。

但是,另一些单元测试框架并不包含与 async 兼容的 ThrowsAsync。如果遇到这种情况,那么可以选择自行创建:

/// <summary>
    /// 确保异步委托抛出异常。
    /// </summary>
    /// <typeparam name="TException">
    /// 预期的异常类型。
    /// </typeparam>
    /// <param name="action">所需测试的异步委托。</param>
    /// <param name="allowDerivedTypes">
    /// 是否应当接受派生类型。
    /// </param>
    public static async Task<TException> ThrowsAsync<TException>(Func<Task> action,
        bool allowDerivedTypes = true)
        where TException : Exception
    {
        try
        {
            await action();
            var name = typeof(Exception).Name;
            Assert.Fail($"Delegate did not throw expected exception {name}.");
            return null;
        }
        catch (Exception ex)
        {
            if (allowDerivedTypes && !(ex is TException))
                Assert.Fail($"Delegate threw exception of type {ex.GetType().Name}" +
                    $", but {typeof(TException).Name} or a derived type was expected.");
            if (!allowDerivedTypes && ex.GetType() != typeof(TException))
                Assert.Fail($"Delegate threw exception of type {ex.GetType().Name}" +
                    $", but {typeof(TException).Name} was expected.");
            return (TException)ex;
        }
    }

可以把这个方法当作其他任意 Assert.ThrowsAsync<TException> 方法来使用。别忘了用 await 等待返回值!

测试错误处理与测试成功场景一样重要,甚至说更为重要也不为过。这是因为,成功场景意味着在软件发布之前,每个人都已经尝试过。当然,如果应用程序行为怪异,那么就存在意料之外的错误情况。

然而,本书建议开发人员摈弃 ExpectedException。比起在测试中随时测试异常,测试从特定节点抛出的异常更为可行。与其使用 ExpectedException,不如使用 ThrowsAsync 或者 ThrowsAsync 实现,就像上例中的那样。

 (完)

相关阅读:


C# 对 async 方法进行单元测试

C# 对预期失败的 async 方法进行单元测试

C# 对 async void 方法进行单元测试

C# 对数据流网格进行单元测试

C# 对 System.Reactive 可观察对象进行单元测试

C# 通过伪造调度对 System.Reactive 可观察对象进行单元测试

评论

此博客中的热门博文

in 参数(C# 7.2)

C# ref 局部变量和 ref return

类 ref 结构体(C# 7.2)