我如何使用Assert(或其他测试类)来验证在使用MSTest/Microsoft.VisualStudio.TestTools.UnitTesting时抛出了异常?


当前回答

我不建议使用ExpectedException属性(因为它约束太大,容易出错),也不建议在每个测试中编写一个try/catch块(因为它太复杂,容易出错)。使用设计良好的断言方法——可以由您的测试框架提供,也可以自己编写。下面是我写的和用的。

public static class ExceptionAssert
{
    private static T GetException<T>(Action action, string message="") where T : Exception
    {
        try
        {
            action();
        }
        catch (T exception)
        {
            return exception;
        }
        throw new AssertFailedException("Expected exception " + typeof(T).FullName + ", but none was propagated.  " + message);
    }

    public static void Propagates<T>(Action action) where T : Exception
    {
        Propagates<T>(action, "");
    }

    public static void Propagates<T>(Action action, string message) where T : Exception
    {
        GetException<T>(action, message);
    }

    public static void Propagates<T>(Action action, Action<T> validation) where T : Exception
    {
        Propagates(action, validation, "");
    }

    public static void Propagates<T>(Action action, Action<T> validation, string message) where T : Exception
    {
        validation(GetException<T>(action, message));
    }
}

使用示例:

    [TestMethod]
    public void Run_PropagatesWin32Exception_ForInvalidExeFile()
    {
        (test setup that might propagate Win32Exception)
        ExceptionAssert.Propagates<Win32Exception>(
            () => CommandExecutionUtil.Run(Assembly.GetExecutingAssembly().Location, new string[0]));
        (more asserts or something)
    }

    [TestMethod]
    public void Run_PropagatesFileNotFoundException_ForExecutableNotFound()
    {
        (test setup that might propagate FileNotFoundException)
        ExceptionAssert.Propagates<FileNotFoundException>(
            () => CommandExecutionUtil.Run("NotThere.exe", new string[0]),
            e => StringAssert.Contains(e.Message, "NotThere.exe"));
        (more asserts or something)
    }

笔记

返回异常而不是支持验证回调是一个合理的想法,只是这样做会使这个断言的调用语法与我使用的其他断言非常不同。

与其他人不同,我使用“propagates”而不是“throws”,因为我们只能测试异常是否从调用中传播。我们不能直接测试是否抛出了异常。但我想你可以把投掷想象成:被扔出去而没有被接住。

最后认为

在切换到这种方法之前,我考虑过在测试只验证异常类型时使用ExpectedException属性,在需要更多验证时使用try/catch块。但是,我不仅要考虑在每个测试中使用哪种技术,而且随着需求的变化将代码从一种技术更改为另一种技术也不是一件简单的工作。使用一种一致的方法可以节省脑力。

总而言之,这种方法具有易用性、灵活性和健壮性(很难做错)。

更新

我的方法对于mstest V2来说不再有价值了,它似乎已经在2018年问世了。使用Assert.ThrowsException。

除非你一直在使用旧版本的mstest。那么,我的方法仍然适用。

其他回答

FluentAssertions例子

为那些使用该库的用户添加一个使用FluentAssertions的示例。

// act
Action result = () => {
    sut.DoSomething();
};

// assert
result.Should().Throw<Exception>();

异步的例子

// act
Func<Task> result = async () => {
    await sut.DoSomethingAsync();
};

// assert
await result.Should().ThrowAsync<Exception>();

Even though this is an old question, I would like to add a new thought to the discussion. I have extended the Arrange, Act, Assert pattern to be Expected, Arrange, Act, Assert. You can make an expected exception pointer, then assert it was assigned to. This feels cleaner than doing your Asserts in a catch block, leaving your Act section mostly just for the one line of code to call the method under test. You also don't have to Assert.Fail(); or return from multiple points in the code. Any other exception thrown will cause the test to fail, because it won't be caught, and if an exception of your expected type is thrown, but the it wasn't the one you were expecting, Asserting against the message or other properties of the exception help make sure your test won't pass inadvertently.

[TestMethod]
public void Bar_InvalidDependency_ThrowsInvalidOperationException()
{
    // Expectations
    InvalidOperationException expectedException = null;
    string expectedExceptionMessage = "Bar did something invalid.";

    // Arrange
    IDependency dependency = DependencyMocks.Create();
    Foo foo = new Foo(dependency);

    // Act
    try
    {
        foo.Bar();
    }
    catch (InvalidOperationException ex)
    {
        expectedException = ex;
    }

    // Assert
    Assert.IsNotNull(expectedException);
    Assert.AreEqual(expectedExceptionMessage, expectedException.Message);
}

通常你的测试框架会给出答案。但如果它不够灵活,你可以这样做:

try {
    somethingThatShouldThrowAnException();
    Assert.Fail(); // If it gets to this line, no exception was thrown
} catch (GoodException) { }

正如@Jonas指出的,这并不适用于捕捉基本异常:

try {
    somethingThatShouldThrowAnException();
    Assert.Fail(); // raises AssertionException
} catch (Exception) {
    // Catches the assertion exception, and the test passes
}

如果绝对必须捕获Exception,则需要重新抛出Assert.Fail()。但实际上,这是一个你不应该手写的信号;检查测试框架中的选项,或者查看是否可以抛出更有意义的异常进行测试。

catch (AssertionException) { throw; }

您应该能够根据自己的需要调整这种方法——包括指定要捕获的异常类型。如果你只期望某些类型,完成catch块:

} catch (GoodException) {
} catch (Exception) {
    // not the right kind of exception
    Assert.Fail();
}

在使用NUnit的情况下,试试这个:

Assert.That(() =>
        {
            Your_Method_To_Test();
        }, Throws.TypeOf<Your_Specific_Exception>().With.Message.EqualTo("Your_Specific_Message"));

这取决于您使用的测试框架?

例如,在MbUnit中,您可以用一个属性指定预期的异常,以确保您得到的是真正预期的异常。

[ExpectedException(typeof(ArgumentException))]