web-dev-qa-db-ja.com

なぜコードコントラクトの例外をキャッチできないのですか?

System.Diagnostics.Contracts.ContractExceptionは、私のテストプロジェクトではアクセスできません。このコードは、純粋に私自身がVisual Studioの光沢のある新しいコピーをいじっていることに注意してください。しかし、私が間違っていることを知りたいのです。

VSのプロフェッショナル版を使用しているため、静的チェックはありません。 (私が好きな)コードコントラクトを引き続き使用するために、私のメソッドが機能する唯一の方法は、実行時にスローされる例外をキャッチすることであると考えましたが、これが可能であるとは思いません。

TestMethod

[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 'は、保護レベルが原因でアクセスできません
。

編集

もう少し考えた後、私はコメントと以下で議論された結論に達しました。メソッドを考えると、これにコードコントラクト形式で表現できる要件がある場合は、そのようにテストを記述します。

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

これにより、コントラクトがコードの一部であり、削除されないことが保証されます。ただし、これには、コードコントラクトが指定された例外を実際にスローする必要があります。ただし、これが必要ない場合もあります。

47
Finglas

これは意図的なものですが、テストには少し苦痛があります。

重要なのは、本番コードでは、コントラクトの例外をキャッチしたくないということです。これはコードのバグを示しているため、次のリクエストに進むことができるように、コールスタックの一番上でキャッチしたい任意の予期しない例外以上のものを期待するべきではありません。基本的に、契約の例外をそのように「処理」できるものと見なすべきではありません。

さて、それをテストするのは苦痛です...しかし、とにかくあなたは本当にあなたの契約をテストしたいですか?これは、コンパイラーがstringパラメーターを持つメソッドにintを渡さないようにするテストに少し似ていませんか?契約を宣言しました。適切に文書化して、適切に実施できます(とにかく設定に基づいて)。

doがコントラクトの例外をテストしたい場合は、テストで裸のExceptionをキャッチしてそのフルネームを確認するか、または Contract.ContractFailed イベント。ユニットテストフレームワークには、時間の経過とともにこれに対するサポートが組み込まれていると思いますが、そこに到達するには少し時間がかかります。それまでの間、契約違反を予期するためのユーティリティメソッドが必要になる可能性があります。 1つの可能な実装:

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.
    }
}
69
Jon Skeet

EDIT:変換が行われ、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");
    }

ExpectedExceptionAttributeと同様に動作する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);
    }
8
Stephen Drew

vs2010 rtmでは、フルネームが「System.Diagnostics.Contracts .__ ContractsRuntime + ContractException」に変更されました。 HTH

2
user341451

この質問は古くなり、回答はすでに提供されていますが、物事をシンプルで読みやすくするための優れたソリューションがあるように感じます。最終的には、次のような単純な前提条件でテストを作成できます。

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

public void FailingPrecondition() {
    Contracts.Require(false);
}

さて、アイデアは、コードコントラクトリライターにカスタムコントラクトランタイムクラスを提供することです。これは、「カスタムリライタメソッド」の下のアセンブリのプロパティで設定できます(コードコントラクトユーザーマニュアルのセクション7.7を参照)。

1

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

これにより、上記のように、前提条件違反に関する簡単で読みやすいテストを作成できます。

1
Mikkel R. Lund