Почему вы не можете поймать исключения Кодового Контракта? - PullRequest
47 голосов
/ 14 апреля 2010

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

Error

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

Это будет гарантировать, что контракт является частью кода и не будет удален. Однако это потребует от Кодового контракта фактического выброса указанного исключения. Однако в некоторых случаях это не требуется.

Ответы [ 4 ]

69 голосов
/ 14 апреля 2010

Это намеренно - хотя небольшая боль для тестирования.

Дело в том, что в производственном коде вы никогда не должны ловить исключение контракта; это указывает на ошибку в вашем коде, поэтому вы не должны ожидать, что произойдет что-то большее, чем произвольные непредвиденные исключения, которые вы, возможно, захотите перехватить прямо на вершине стека вызовов, чтобы перейти к следующему запросу. По сути, вы не должны рассматривать исключения из контракта как те, которые могут быть «обработаны» как таковые.

Теперь, для тестирования это боль ... но вы действительно хотите проверить свои контракты в любом случае? Разве это не похоже на тестирование того, что компилятор мешает вам передать 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.
    }
}
8 голосов
/ 05 августа 2011

РЕДАКТИРОВАТЬ: У меня было преобразование, и я больше не использую 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, который ведет себя аналогично 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
                )
            );
        }
    }
}

И это можно использовать аналогично:

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

        MyClass mc = new MyClass(arg);
    }
2 голосов
/ 14 мая 2010

в версии vs2010, полное имя было изменено на «System.Diagnostics.Contracts .__ ContractsRuntime + ContractException». НТН

1 голос
/ 16 января 2016

Хотя этот вопрос стареет, а ответ уже предоставлен, я чувствую, что у меня есть хорошее решение, которое делает вещи простыми и читаемыми. В конце концов, это позволяет нам писать тесты на предварительных условиях просто:

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

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

Хорошо, так что идея состоит в том, чтобы предоставить переписчику Code Contracts собственный класс времени исполнения контракта. Это можно настроить в свойствах сборки в разделе «Пользовательские методы перезаписи» (см. Раздел 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>();
}

Это позволяет нам писать простые читаемые тесты на нарушения предварительных условий, как показано выше.

...