异步方法中的单元测试异常

Unit testing exception in async method

我正在尝试使用 VSTest 对下面的异步方法进行单元测试。但是,即使仅在第一种情况下抛出异常,AsyncMathsStatic.Divide(4, 0)AsyncMathsStatic.Divide(4, 1)) 的测试也通过了。

[TestClass]
public class UnitTest1
{
    [TestMethod]

    public void DivideTest1()
    {
        // Func<Task> action = async()=> {  AsyncMathsStatic.Divide(4, 0); };
        //action.Should().Throw<DivideByZeroException>();

        Assert.ThrowsExceptionAsync<DivideByZeroException>(async () => 
        AsyncMathsStatic.Divide(4, 0));
    }
}

public class AsyncMathsStatic
{
    public static async void Divide(int v1, int v2)
    {
        try
        {
            if (v1/v2 > 1)
            {
                // do something time consuming
            }   
        }
        catch (DivideByZeroException ex)
        {
            throw;
        }
    }
}

了解异步方法的工作原理对于了解这里发生了什么很重要。所有异步方法都同步启动 运行,就像任何其他方法一样。但是第一个await作用于一个不完整的Task方法returns。通常它会 return 它自己的 Task 调用者然后可以等待。

但是如果方法签名是async void,那么return就什么都不是了。那时,该方法尚未完成 运行,但调用方法永远不知道何时或是否完成。

这很可能就是这里发生的事情。该方法在遇到第一个 await 时正在 returning 并且它认为测试成功完成。它永远不会看到稍后抛出的异常。

修复是return一个Task,所以方法可以等待:

public static async Task Divide(int v1, int v2)

async void 的唯一合法用途是用于事件处理程序,因为您别无选择,只能制作它们 void。但它通常也没有问题,因为事件应该是 "oh by the way this happened" 并且事件处理程序的成功完成通常不会影响调用它的任何操作的进一步操作(但也有一些例外)。

如果您的测试环境不允许使用异步测试方法,请使用如下内容:

       [TestMethod]
    public void TestMethod1()
    {
        Task.Run(async () =>
        {
           // test setup

            var result = await SomeAsyncTask();
            Assert.IsTrue(result.Id != 0, "Id not changed");

        }).GetAwaiter().GetResult();
    }

正如@Paulo 所指出的,如果您的测试环境确实允许异步测试方法,那么您可以使用:

      [TestMethod]
    public async Task TestMethod1()
    {
       // test setup

        var result = await SomeAsyncMethod();
        Assert.IsTrue(result.Id != 0, "Id not changed");
    }

即使方法是同步的,执行也将始终是同步的,直到第一次等待未完成的任务。您的代码甚至没有 awaits.

async void 方法无法等待。

方法返回未等待的任务,就好像该方法是 async void

以下代码显示了所有这些情况:

static async Task Main()
{
    try
    {
        Console.WriteLine("Before calling "+ nameof(AsyncMathsStatic.Divide1));
        var t = AsyncMathsStatic.Divide1(4, 0);
        Console.WriteLine("After calling "+ nameof(AsyncMathsStatic.Divide1));
        await t;
    }
    catch (DivideByZeroException ex)
    {
        Console.WriteLine("Exception thrown in " + nameof(Main));
    }

    try
    {
        Console.WriteLine("Before calling "+ nameof(AsyncMathsStatic.Divide2));
        var t = AsyncMathsStatic.Divide2(4, 0);
        Console.WriteLine("After calling "+ nameof(AsyncMathsStatic.Divide2));
        await t;
    }
    catch (DivideByZeroException ex)
    {
        Console.WriteLine("Exception thrown in " + nameof(Main));
    }
}

public class AsyncMathsStatic
{
    public static async Task Divide1(int v1, int v2)
    {
        try
        {
            Console.WriteLine(nameof(Divide1) + ".1");
            await Task.Yield();
            Console.WriteLine(nameof(Divide1) + ".2");
            if (v1 / v2 > 1)
            {
                await Task.Yield();
            }
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine("Exception thrown in " + nameof(Divide1));
            throw;
        }
    }
    public static async Task Divide2(int v1, int v2)
    {
        try
        {
            Console.WriteLine(nameof(Divide2) + ".1");
            await Task.CompletedTask;
            Console.WriteLine(nameof(Divide2) + ".2");
            if (v1 / v2 > 1)
            {
                await Task.Yield();
            }
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine("Exception thrown in " + nameof(Divide2));
            throw;
        }
    }
}

/* Output
Before calling Divide1
Divide1.1
After calling Divide1
Divide1.2
Exception thrown in Divide1
Exception thrown in Main
Before calling Divide2
Divide2.1
Divide2.2
Exception thrown in Divide2
After calling Divide2
Exception thrown in Main
*/

这证明,如果与您发布的完全一样,您应该可以抛出。

我是不是遗漏了什么?