为什么你不能抓住代码合同例外?

Fin*_*las 47 c# .net-4.0 visual-studio-2010 code-contracts

在我的测试项目中无法访问System.Diagnostics.Contracts.ContractException.请注意,这段代码纯粹是我自己搞乱了我的Visual Studio新副本,但我想知道我做错了什么.

我正在使用VS的专业版,因此​​我没有静态检查.为了仍然使用代码契约(我喜欢),我认为我的方法可以工作的唯一方法是捕获在运行时抛出的异常,但我发现这不可能.

测试方法

[TestMethod, ExpectedException(typeof(System.Diagnostics.Contracts.ContractException))]
public void returning_a_value_less_than_one_throws_exception()
{
    var person = new Person();
    person.Number();
}
Run Code Online (Sandbox Code Playgroud)

方法

public int Number()
{
    Contract.Ensures(Contract.Result<int>() >= 0);
    return -1;
}
Run Code Online (Sandbox Code Playgroud)

错误

Error 1 'System.Diagnostics.Contracts.ContractException' is inaccessible
due to its protection level.

编辑

经过一番思考后,我得出了评论中讨论的结论,以及以下内容.给定一种方法,如果这有一个可以用代码合同形式表达的要求,我会写这样的测试.

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void value_input_must_be_greater_than_zero()
{
    // Arrange
    var person = new Person();
    // Act
    person.Number(-1);
}
Run Code Online (Sandbox Code Playgroud)

这将确保合同是代码的一部分,并且不会被删除.这将要求Code Contract实际抛出指定的异常.但在某些情况下,不需要这样做.

Jon*_*eet 69

这是故意的 - 尽管测试有点痛苦.

关键在于,在生产代码中,您永远不应该想要捕获合同异常; 它表示代码中存在错误,因此您不应期望任何可能需要在调用堆栈顶部捕获的任意意外异常,以便您可以转到下一个请求.基本上,您不应将合同例外视为可以"处理"的合同例外.

现在,为了测试这是一个痛苦......但你真的想测试你的合同吗?这有点像测试编译器阻止你传入一个stringint参数的方法吗?您已经声明了合同,可以对其进行适当的记录,并进行适当的强制执行(无论如何都要根据设置).

如果您确实想要测试合同例外,您可以Exception在测试中查看并检查其全名,或者您可以处理该Contract.ContractFailed事件.我希望单元测试框架能够随着时间的推移对其进行内置支持 - 但要实现这一目标需要一段时间.与此同时,您可能希望使用实用程序方法来预期合同违规.一种可能的实现:

const string ContractExceptionName =
    "System.Diagnostics.Contracts.__ContractsRuntime.ContractException";

public static void ExpectContractFailure(Action action)
{
    try
    {
        action();
        Assert.Fail("Expected contract failure");
    }
    catch (Exception e)
    {
        if (e.GetType().FullName != ContractExceptionName)
        {
            throw;
        }
        // Correct exception was thrown. Fine.
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @Jason:您是否也测试过不能使用错误的参数类型调用该方法?我的意思是方法签名肯定也是规范的一部分......所以你有一个单元测试,它使用反射来尝试调用一个接受字符串的方法,但是使用一个int参数?如果没有,这是不是意味着你没有完全测试规格?*那种*声明方面(方法签名)和合同之间有什么区别?我们可能不得不同意最终的不同,但这是一个有趣的讨论...... (14认同)
  • @Jon Skeet:我不确定我是否同意合同不需要测试.更具体地说,它不是正在测试的合同,而是违反合同时该方法将抛出的行为.假设一个方法`void M(string s)`的规范要求`M`抛出,如果`s`为null或空格.实现这一点的一种方法是使用`Contract.Requires`.另一种方法是经典的`if-throw`结构,或者可能使用内置的`Guard.Against`.关键是,测试不应该测试合同,而是"M"符合规范. (9认同)
  • @Finglas:我个人*不认为合同需要测试 - 至少除非他们很复杂,他们通常不应该.然而,这只是个人观点,你应该*绝对*期望听到其他人.想想我写的是什么,但请不要有任何同意的义务:)希望如果你觉得你*想要测试合同,答案中的代码会帮助你. (7认同)
  • @piers7:在这种情况下,我可能会在程序集中使用*one*方法 - 这并不是说你不小心会为那个方法而不是其他方法重写.为每一份合同添加单元测试对我来说都是浪费时间. (4认同)
  • @BernhardHofmann:但静态分析工具也会阻止你用null参数调用方法......当然,如果你正在使用静态分析工具. (3认同)
  • @Jason:但合同*是*规范.如果以声明的方式表达,可以自动记录,查看等方式,测试的好处是什么?我可以理解是否有一个*复杂*合约可能需要一些例子来解释,但我看不出简单的null参数的好处. (2认同)
  • @乔恩斯基特:不同意。该合同是规范的一部分的实现。因此,如果我要实现一个方法“void M(string s)”,如果“s”为空或空格则抛出异常,否则将“s”打印到控制台,这就是规范。如果忽略包含“Contract.Requires”或“if-throw”构造,我可能无法完全实现该规范。单元测试的目的是确保我完全实现规范,并防止将来的重构和维护。 (2认同)
  • @Jon Skeet:合同很简单,但是没有激活编译时重写,所以合同什么都不做.拒绝坏参数的单元测试有助于避免这种情况(即使您正在测试的是代码合同实现),尽管我会尽量避免让测试"知道"CodeContracts是实现. (2认同)
  • @JonSkeet @Jason *对我来说*,关键的区别是编译器将阻止我使用 `int` 参数调用 `void M(string s)`。单元测试应该测试代码_行为_,因此如果我在使用 null 调用它时预计会出现异常,那么我应该能够通过单元测试来验证预期的行为。 (2认同)
  • 乔恩 - 你是对的,ExpectedException 和其他属性不是正确的选择。我编写了一个类似于 xUnit 的断言扩展 Assert.Throws&lt;T&gt;() ,这更干净、更精确。至于测试合约,你当然应该这样做。如果有人删除或修改合约而导致行为发生变化,您会得到一个失败的测试,表明发生了重大更改。这对于开发库尤其重要。 (2认同)
  • 我在这里的对话有点晚了,但我相信方法签名和契约之间的主要区别在于契约默认情况下不会破坏构建。当需要字符串时,无法编译使用 int 调用方法的代码。有可能(通过配置)以违反约定的方式调用方法。所以我想说,如果您的构建系统要求静态分析器变绿,那么就放弃测试。如果您的构建系统不依赖于静态分析器通过,则添加测试。 (2认同)

Ste*_*rew 8

编辑:我有一个转换,不再使用ExpectedException或下面的这个属性,而是编写了一些扩展方法:

AssertEx.Throws<T>(Action action);
AssertEx.ThrowsExact<T>(Action action);
AssertEx.ContractFailure(Action action);
Run Code Online (Sandbox Code Playgroud)

这些允许我更准确地提出异常的位置.

ContractFailure方法示例:

    [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cannot catch ContractException")]
    public static void ContractFailure(Action operation)
    {
        try
        {
            operation();
        }
        catch (Exception ex)
        {
            if (ex.GetType().FullName == "System.Diagnostics.Contracts.__ContractsRuntime+ContractException")
                return;

            throw;
        }

        Assert.Fail("Operation did not result in a code contract failure");
    }
Run Code Online (Sandbox Code Playgroud)

我为MSTest创建了一个属性,其行为类似于ExpectedExceptionAttribute:

public sealed class ExpectContractFailureAttribute : ExpectedExceptionBaseAttribute
{
    const string ContractExceptionName = "System.Diagnostics.Contracts.__ContractsRuntime+ContractException";

    protected override void Verify(Exception exception)
    {
        if (exception.GetType().FullName != ContractExceptionName)
        {
            base.RethrowIfAssertException(exception);
            throw new Exception(
                string.Format(
                    CultureInfo.InvariantCulture,
                    "Test method {0}.{1} threw exception {2}, but contract exception was expected. Exception message: {3}",
                    base.TestContext.FullyQualifiedTestClassName,
                    base.TestContext.TestName,
                    exception.GetType().FullName,
                    exception.Message
                )
            );
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这可以类似地使用:

    [TestMethod, ExpectContractFailure]
    public void Test_Constructor2_NullArg()
    {
        IEnumerable arg = null;

        MyClass mc = new MyClass(arg);
    }
Run Code Online (Sandbox Code Playgroud)