【问题标题】:How come you cannot catch Code Contract exceptions?为什么你不能捕获代码合同异常?
【发布时间】:2010-04-14 18:26:17
【问题描述】:

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();
}

方法

public int Number()
{
    Contract.Ensures(Contract.Result<int>() >= 0);
    return -1;
}

错误

错误 1 ​​'System.Diagnostics.Contracts.ContractException' 不可访问
由于其保护级别。

编辑

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

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

这将确保合约是代码的一部分,并且不会被删除。然而,这将要求代码合同实际抛出指定的异常。但是,在某些情况下,这不是必需的。

【问题讨论】:

标签: c# visual-studio-2010 .net-4.0 code-contracts


【解决方案1】:

这是故意的 - 尽管测试时会有些痛苦。

关键是在生产代码中你永远不应该想捕获合约异常;它表明您的代码中存在错误,因此您不应该期望任何意外异常,您可能希望在调用堆栈的顶部捕获这些异常,以便您可以继续下一个请求。基本上,您不应将合同例外视为可以“处理”的例外。

现在,测试是一件很痛苦的事情……但是你真的想测试你的合同吗?这不是有点像测试编译器阻止您将string 传递给具有int 参数的方法吗?您已经声明了合同,它可以被适当地记录并适当地执行(无论如何,基于设置)。

如果您确实想要测试合约异常,您可以在测试中捕获一个裸露的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.
    }
}

【讨论】:

  • @Finglas:我个人认为合约不需要测试——至少除非它们很复杂,而且通常不应该这样。不过,这只是个人观点,您应该绝对期待听到其他人的声音。想想我写的东西,但请不要觉得有义务同意 :) 如果您觉得确实想要测试合同,希望答案中的代码对您有所帮助。
  • @Jon Skeet:我不确定我是否同意合同不需要测试。更具体地说,测试的不是合约,而是合约被违反时该方法将抛出的行为。假设void M(string s) 方法的规范要求M throw 如果s 为空或空白。实现这一点的一种方法是使用Contract.Requires。另一种方法是经典的if-throw 构造,或者可能使用内置的Guard.Against。关键是,测试不应该是在测试合约,而是 M 符合规范。
  • @Jason:您是否也测试过不能使用错误的参数类型调用该方法?我的意思是,方法签名当然也是规范的一部分......所以你有一个单元测试,它使用反射来尝试调用一个采用字符串但使用 int 参数的方法?如果没有,这是否意味着您没有完全测试规范? 那种的声明性方面(方法签名)和合同之间有什么区别?我们最终可能不得不同意不同,但这是一个有趣的讨论......
  • @piers7:在这种情况下,我可能会为程序集中的 one 方法执行此操作 - 这不像您会不小心为那个方法打开重写方法而不是其他方法。为每份合同添加单元测试对我来说就像是在浪费时间。
  • @BernhardHofmann:但是静态分析工具也会阻止您使用 null 参数调用方法……当然,如果您使用的是静态分析工具。
【解决方案2】:

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

AssertEx.Throws<T>(Action action);
AssertEx.ThrowsExact<T>(Action action);
AssertEx.ContractFailure(Action action);

这些让我可以更准确地了解引发异常的位置。

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");
    }

我为 MSTest 创建了一个属性,其行为类似于 预期异常属性:

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
                )
            );
        }
    }
}

这可以类似地使用:

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

        MyClass mc = new MyClass(arg);
    }

【讨论】:

    【解决方案3】:

    在vs2010 rtm中,全名已更改为“System.Diagnostics.Contracts.__ContractsRuntime+ContractException”。高温

    【讨论】:

      【解决方案4】:

      虽然这个问题已经过时了,并且已经提供了答案,但我觉得我有一个很好的解决方案,可以让事情变得简单易读。 最后,它允许我们在前置条件上编写测试,如下所示:

      [Test]
      public void Test()
      {
          Assert.That(FailingPrecondition, Violates.Precondition);
      }
      
      public void FailingPrecondition() {
          Contracts.Require(false);
      }
      

      好的,我们的想法是为代码契约重写器提供一个自定义契约运行时类。 这可以在“自定义重写器方法”下的程序集属性中进行设置(请参阅代码合同用户手册第 7.7 节):

      记得检查Call-site Requires Checking

      自定义类如下所示:

      public static class TestFailureMethods
      {
          public static void Requires(bool condition, string userMessage, string conditionText)
          {
              if (!condition)
              {
                  throw new PreconditionException(userMessage, conditionText);
              }
          }
      
          public static void Requires<TException>(bool condition, string userMessage, string conditionText) where TException : Exception
          {
              if (!condition)
              {
                  throw new PreconditionException(userMessage, conditionText, typeof(TException));
              }
          }
      }
      

      使用自定义的PreconditionException 类(它没有任何花哨的东西!)。 我们还添加了一个小助手类:

      public static class Violates
      {
          public static ExactTypeConstraint Precondition => Throws.TypeOf<PreconditionException>();
      }
      

      这使我们能够编写简单易读的前置条件违规测试,如上所示。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-11-24
        • 1970-01-01
        • 1970-01-01
        • 2021-03-16
        • 2013-03-03
        • 2012-08-27
        • 2013-10-26
        相关资源
        最近更新 更多