单元测试中try/catch未捕获的异常

3

我现在正在设计一些代码,当一个字符串参数为null或空时,我会抛出一个异常。异常已经被正确地抛出了,但是在进行单元测试时却无法捕获。

这是我使用的客户端。

public class PipeClient : IPipeClient
{
    public async void Send(string host, string pipeName, Message msg)
    {
        if (string.IsNullOrEmpty(msg.PreparedMessage))
            throw new ArgumentException("MESSAGE_NOT_FOUND");

        if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(pipeName))
            throw new ArgumentNullException();

        if (!host.TryParseHost()) 
            throw new ArgumentException("INVALID_HOST_NAME");

        using (var pipeClient = new NamedPipeClientStream(host, pipeName, PipeDirection.Out))
        {
            pipeClient.Connect(200);

            using (var writer = new StreamWriter(pipeClient))
            {
                await Task.Run(() => writer.WriteLine(msg.PreparedMessage));
                writer.Flush();
            }
        }
    }
}

这里是单元测试(Unit Test)的代码:

    [TestMethod]
    public void Send_FailsOnWrongHostName()
    {
        var name = "FailWithHostname";
        var msg = new Message(MyStates.Register, "UnitTest", "Test");

        try
        {
            var client = new PipeClient();
            client.Send("lol", name, msg);
        }
        catch (Exception e)
        {
            Assert.IsTrue(e is ArgumentException);
        }
    }

据我所知,运行该测试时,当我调用Send方法时会抛出异常,然后在catch子句中被捕获,因为我没有在PipeClient内捕获它。但事实并非如此,它只是以测试失败的状态退出。

如果您需要更多信息,请告诉我,提前感谢您。


1
你不需要捕获和测试异常。如果你期望出现异常,那么在[TestMethod]上方添加[ExpectedException(typeof(ArgumentException))]的注释即可。 - IronAces
我当然已经通过调试确认该方法确实抛出了预期的异常。 - M. Pedersen
你确定抛出的异常实际上是 ArgumentException 吗?当查看您的代码并假定确实发生异常时,我唯一能想象的是 Send 方法中的某个方法调用会抛出一个不同于 ArgumentException 类型的异常。 - user2819245
@elgonzo 抱歉之前我应该表述得更清楚一些。我确认了异常被抛出,而且是一个ArgumentException。 - M. Pedersen
@Nkosi 谢谢,我会去了解一下! - M. Pedersen
显示剩余3条评论
1个回答

2

这里有几件事情我想在这个回答中提出。我不确定你的经验水平,所以请不要认为我在任何时候都是轻蔑的。

首先,简要介绍异步方法和任务。

  • 应避免使用Async void,除非在异步事件处理程序中。异步方法应返回Task或Task,否则调用方法将无法保持知道方法何时完成并报告方法是否引发异常。异步void本质上是“fire and forget”,没有人留下来观察异常。

"在观察到的任务中,没有人能听到你的尖叫声" -我,2018年

  • 在异步方法中引发的异常会在等待异步方法时被很好地解包并抛出,调用堆栈也会被保留并且相当合理。如果您在未来某个时间点没有等待结果,那么您将收到UnobservedTaskException,如果您没有为其配置全局处理程序,则会导致应用程序崩溃。如果您使用.Wait()、.Result或通过.GetAwaiter().GetResult()同步获取异步方法的结果(所有3个选项都应尽量避免,但如果必须这样做,第3个选项是最好的,我已经得到了通知),那么您将获得包装在AggregateException中的原始异常。

如果这些都不太清楚,我建议您阅读有关任务和异步/等待的一些资料。

现在让我们来看看您的测试。

您的方法是异步void,因此对于调用方法来说没有任何东西可以返回,以代表工作或让它知道该方法是否引发了异常。所以它继续运行,测试结束,然后一切都完成了,因为UnobservedTaskException可以在将来的任何时候抛出(我认为它与垃圾收集器整理故障任务的时间有关,然后它会抛出异常,因为垃圾收集器是非确定性的,所以我们无法确定何时会发生)。

那么,如果您使您的异步方法返回一个任务呢???那还不太对。您现在返回了一个处于故障状态的任务,因为存在异常,但是由于您从未等待它,所以异常从未被“解包”并实际抛出,因此您的测试仍然很愉快地继续进行。

您需要做的是使您的测试异步并返回一个任务,并使您要测试的方法异步任务而不是异步void,并在测试中等待该方法。

像这样:

[TestMethod]
public async Task Send_FailsOnWrongHostName()
{
    var name = "FailWithHostname";
    var msg = new Message(MyStates.Register, "UnitTest", "Test");

    try
    {
        var client = new PipeClient();
        await client.Send("lol", name, msg);
    }
    catch (Exception e)
    {
        Assert.IsTrue(e is ArgumentException);
    }
}

public class PipeClient : IPipeClient
{
    public async Task Send(string host, string pipeName, Message msg)
    {
        if (string.IsNullOrEmpty(msg.PreparedMessage))
            throw new ArgumentException("MESSAGE_NOT_FOUND");

        if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(pipeName))
            throw new ArgumentNullException();

        if (!host.TryParseHost()) 
            throw new ArgumentException("INVALID_HOST_NAME");

        using (var pipeClient = new NamedPipeClientStream(host, pipeName, PipeDirection.Out))
        {
            pipeClient.Connect(200);

            using (var writer = new StreamWriter(pipeClient))
            {
                await Task.Run(() => writer.WriteLine(msg.PreparedMessage));
                writer.Flush();
            }
        }
    }
}

1
首先,感謝你這么詳盡的解釋!當我在進行代碼審查和重構時發現自己完全忘記了把方法設置成異步的。這就是幾個月后回來看沒有注釋或解釋的代碼所面臨的問題。我實際上不知道一個 async void 方法不會將異常返回給調用者,但這也很合理。等我明天回家後試試,然后再更新結果。再次感謝! - M. Pedersen

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