Лучший способ проверить исключения с помощью Assert, чтобы убедиться, что они будут выброшены

97

Как вы думаете, это хороший способ тестирования исключений? Какие-либо предложения?

Exception exception = null;
try{
    //I m sure that an exeption will happen here
}
catch (Exception ex){
    exception = ex;
}

Assert.IsNotNull(exception);

Я использую MS Test.

Ханнун Яссир
источник

Ответы:

137

У меня есть несколько разных шаблонов, которые я использую. Я использую ExpectedExceptionатрибут чаще всего, когда ожидается исключение. Этого достаточно для большинства случаев, однако бывают случаи, когда этого недостаточно. Исключение может быть невозможно перехватить - поскольку оно вызывается методом, вызываемым отражением, - или, возможно, я просто хочу проверить, выполняются ли другие условия, например, откат транзакции или какое-то значение все еще установлено. В этих случаях я помещаю его в try/catchблок, который ожидает точное исключение, выполняет Assert.Failли код, если код завершается успешно, а также перехватывает общие исключения, чтобы убедиться, что другое исключение не сгенерировано.

Первый случай:

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void MethodTest()
{
     var obj = new ClassRequiringNonNullParameter( null );
}

Второй случай:

[TestMethod]
public void MethodTest()
{
    try
    {
        var obj = new ClassRequiringNonNullParameter( null );
        Assert.Fail("An exception should have been thrown");
    }
    catch (ArgumentNullException ae)
    {
        Assert.AreEqual( "Parameter cannot be null or empty.", ae.Message );
    }
    catch (Exception e)
    {
        Assert.Fail(
             string.Format( "Unexpected exception of type {0} caught: {1}",
                            e.GetType(), e.Message )
        );
    }
}
tvanfosson
источник
16
Многие фреймворки модульного тестирования реализуют ошибки утверждения как исключения. Таким образом, Assert.Fail () во втором случае будет пойман блоком catch (Exception), который скроет сообщение об исключении. Вам нужно добавить уловку (NUnit.Framework.AssertionException) {throw;} или что-то подобное - см. Мой ответ.
GrahamS
@Graham - Я набрал это в уме. Обычно я бы также распечатал сообщение об исключении в дополнение к его типу. Дело в том, что тест завершится неудачно, поскольку второй обработчик поймает ошибку утверждения и «повторно выполнит» информацию об ошибке.
tvanfosson
1
Хотя ваш код функционально исправен, я не рекомендую использовать атрибут ExpectedException (поскольку он слишком ограничивает и подвержен ошибкам) ​​или писать блок try / catch в каждом тесте (поскольку он слишком сложен и подвержен ошибкам). Используйте хорошо продуманный метод assert - предоставленный вашей тестовой платформой или напишите свой собственный. Вы можете добиться лучшего кода, и вам не придется выбирать между различными методами или переходить с одного на другой по мере изменения теста. См. Stackoverflow.com/a/25084462/2166177
Стив
К вашему сведению - я перешел на использование xUnit, у которого есть строго типизированный Assert.Throwsметод, который охватывает оба этих случая.
tvanfosson
Атрибут ExpectedException - неприятный и устаревший способ проверить, выбрасываются ли исключения. Смотрите мой полный ответ ниже.
bytedev
45

Теперь, в 2017 году, вы можете сделать это проще с новой MSTest V2 Framework :

Assert.ThrowsException<Exception>(() => myClass.MyMethodWithError());

//async version
await Assert.ThrowsExceptionAsync<SomeException>(
  () => myObject.SomeMethodAsync()
);
Икаро Бомбонато
источник
Это будет успешным, только если System.Exceptionбудет брошено. Любой другой, например, не System.ArgumentExceptionпройдёт тест.
sschoof
2
Если вы ожидаете другого типа исключения, вы должны проверить его ... В вашем примере вы должны сделать: Assert.ThrowsException <ArgumentException> (() => myClass.MyMethodWithError ());
Икаро Бомбонато
2
Важно отметить, что при использовании Assert.ThrowsException<MyException>будет проверяться исключительно предоставленный тип исключения, а не какие-либо производные от него типы исключений. В моем примере, если тестируемое Subбыл Throwв MyInheritedException(производный тип из базового класса MyException), то тест будет терпеть неудачу .
Ама
Если вы хотите расширить свой тест и принять тип исключения, а также его производные типы, используйте файл Try { SubToTest(); Assert.Fail("...") } Catch (AssertFailedException e) {throw;} Catch (MyException e) {...}. Обратите внимание на первостепенную важность Catch (AssertFailedException e) {throw;}(см. Комментарий allgeek)
Ама
16

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

try
{
    SomethingThatCausesAnException();
    Assert.Fail("Should have exceptioned above!");
}
catch (Exception ex)
{
    // whatever logging code
}

Во всех фреймворках модульного тестирования, с Assert.Failкоторыми я знаком, они генерируют исключение, поэтому общий улов фактически маскирует неудачу теста. Если SomethingThatCausesAnException()не бросает,Assert.Fail будет, но никогда не выйдет из-под контроля бегуна, чтобы указать на сбой.

Если вам нужно перехватить ожидаемое исключение (т. Е. Подтвердить определенные детали, такие как сообщение / свойства исключения), важно перехватить конкретный ожидаемый тип, а не базовый класс Exception. Это позволит Assert.Failисключению всплыть (при условии, что вы не генерируете тот же тип исключения, что и ваша платформа модульного тестирования), но все же разрешите проверку исключения, которое было создано вашим SomethingThatCausesAnException()методом.

Allgeek
источник
15

Начиная с версии 2.5, NUnit имеет следующие уровни методов Assertдля тестирования исключений:

Assert.Throws , который проверит точный тип исключения:

Assert.Throws<NullReferenceException>(() => someNullObject.ToString());

И Assert.Catch, который будет проверять исключение данного типа или типа исключения, производного от этого типа:

Assert.Catch<Exception>(() => someNullObject.ToString());

Кроме того, при отладке модульных тестов, которые генерируют исключения, вы можете захотеть предотвратить прерывание VS при исключении .

редактировать

Чтобы привести пример комментария Мэтью ниже, возврат универсального Assert.Throwsи Assert.Catchявляется исключением с типом исключения, которое вы затем можете изучить для дальнейшей проверки:

// The type of ex is that of the generic type parameter (SqlException)
var ex = Assert.Throws<SqlException>(() => MethodWhichDeadlocks());
Assert.AreEqual(1205, ex.Number);
StuartLC
источник
2
Рой Ошеров рекомендует это в «Искусство модульного тестирования», второе издание, раздел 2.6.2.
Avi
2
Мне нравится Assert.Throws, кроме того, он возвращает исключение, поэтому вы можете писать дальнейшие утверждения о самом исключении.
Мэтью,
Вопрос был для MSTest, а не для NUnit.
bytedev
Исходный вопрос @nashwan OP не имел этой квалификации, и теги по-прежнему не соответствуют требованиям MS-Test. В его нынешнем виде это вопрос C #, .Net, Unit-Testing.
StuartLC
11

К сожалению, MSTest STILL действительно имеет только атрибут ExpectedException (просто показывает, насколько MS заботится о MSTest), который IMO довольно ужасен, потому что он нарушает шаблон Arrange / Act / Assert и не позволяет вам точно указать, в какой строке кода вы ожидаете исключение происходить дальше.

Когда я использую (/ по принуждению клиента) использовать MSTest, я всегда использую этот вспомогательный класс:

public static class AssertException
{
    public static void Throws<TException>(Action action) where TException : Exception
    {
        try
        {
            action();
        }
        catch (Exception ex)
        {
            Assert.IsTrue(ex.GetType() == typeof(TException), "Expected exception of type " + typeof(TException) + " but type of " + ex.GetType() + " was thrown instead.");
            return;
        }
        Assert.Fail("Expected exception of type " + typeof(TException) + " but no exception was thrown.");
    }

    public static void Throws<TException>(Action action, string expectedMessage) where TException : Exception
    {
        try
        {
            action();
        }
        catch (Exception ex)
        {
            Assert.IsTrue(ex.GetType() == typeof(TException), "Expected exception of type " + typeof(TException) + " but type of " + ex.GetType() + " was thrown instead.");
            Assert.AreEqual(expectedMessage, ex.Message, "Expected exception with a message of '" + expectedMessage + "' but exception with message of '" + ex.Message + "' was thrown instead.");
            return;
        }
        Assert.Fail("Expected exception of type " + typeof(TException) + " but no exception was thrown.");
    }
}

Пример использования:

AssertException.Throws<ArgumentNullException>(() => classUnderTest.GetCustomer(null));
bytedev
источник
10

В качестве альтернативы использованию ExpectedExceptionатрибута я иногда определяю два полезных метода для своих тестовых классов:

AssertThrowsException() принимает делегата и утверждает, что он генерирует ожидаемое исключение с ожидаемым сообщением.

AssertDoesNotThrowException() принимает тот же делегат и утверждает, что он не создает исключения.

Это соединение может быть очень полезным, если вы хотите проверить, что в одном случае возникает исключение, а в другом - нет.

Используя их, мой код модульного теста может выглядеть так:

ExceptionThrower callStartOp = delegate(){ testObj.StartOperation(); };

// Check exception is thrown correctly...
AssertThrowsException(callStartOp, typeof(InvalidOperationException), "StartOperation() called when not ready.");

testObj.Ready = true;

// Check exception is now not thrown...
AssertDoesNotThrowException(callStartOp);

Красиво и аккуратно, да?

My AssertThrowsException()и AssertDoesNotThrowException()методы определены в общем базовом классе следующим образом:

protected delegate void ExceptionThrower();

/// <summary>
/// Asserts that calling a method results in an exception of the stated type with the stated message.
/// </summary>
/// <param name="exceptionThrowingFunc">Delegate that calls the method to be tested.</param>
/// <param name="expectedExceptionType">The expected type of the exception, e.g. typeof(FormatException).</param>
/// <param name="expectedExceptionMessage">The expected exception message (or fragment of the whole message)</param>
protected void AssertThrowsException(ExceptionThrower exceptionThrowingFunc, Type expectedExceptionType, string expectedExceptionMessage)
{
    try
    {
        exceptionThrowingFunc();
        Assert.Fail("Call did not raise any exception, but one was expected.");
    }
    catch (NUnit.Framework.AssertionException)
    {
        // Ignore and rethrow NUnit exception
        throw;
    }
    catch (Exception ex)
    {
        Assert.IsInstanceOfType(expectedExceptionType, ex, "Exception raised was not the expected type.");
        Assert.IsTrue(ex.Message.Contains(expectedExceptionMessage), "Exception raised did not contain expected message. Expected=\"" + expectedExceptionMessage + "\", got \"" + ex.Message + "\"");
    }
}

/// <summary>
/// Asserts that calling a method does not throw an exception.
/// </summary>
/// <remarks>
/// This is typically only used in conjunction with <see cref="AssertThrowsException"/>. (e.g. once you have tested that an ExceptionThrower
/// method throws an exception then your test may fix the cause of the exception and then call this to make sure it is now fixed).
/// </remarks>
/// <param name="exceptionThrowingFunc">Delegate that calls the method to be tested.</param>
protected void AssertDoesNotThrowException(ExceptionThrower exceptionThrowingFunc)
{
    try
    {
        exceptionThrowingFunc();
    }
    catch (NUnit.Framework.AssertionException)
    {
        // Ignore and rethrow any NUnit exception
        throw;
    }
    catch (Exception ex)
    {
        Assert.Fail("Call raised an unexpected exception: " + ex.Message);
    }
}
GrahamS
источник
4

В большинстве сред модульного тестирования .net вы можете поместить атрибут [ExpectedException] в метод тестирования. Однако это не может сказать вам, что исключение произошло в тот момент, когда вы этого ожидали. Вот где может помочь xunit.net .

С xunit у вас есть Assert.Throws, поэтому вы можете делать что-то вроде этого:

    [Fact]
    public void CantDecrementBasketLineQuantityBelowZero()
    {
        var o = new Basket();
        var p = new Product {Id = 1, NetPrice = 23.45m};
        o.AddProduct(p, 1);
        Assert.Throws<BusinessException>(() => o.SetProductQuantity(p, -3));
    }

[Fact] - это xunit-эквивалент [TestMethod]

Стив Уиллкок
источник
Если вам необходимо использовать MSTest (к которому меня часто принуждают работодатели), посмотрите мой ответ ниже.
bytedev
4

Отметьте тест атрибутом ExpectedExceptionAttribute (это термин в NUnit или MSTest; пользователям других платформ модульного тестирования может потребоваться перевод).

Itowlson
источник
Не используйте ExpectedExceptionAttribute (причина указана в моем сообщении ниже). NUnit имеет Assert.Throws <YourException> (), а для MSTest используйте что-то вроде моего класса AssertException ниже.
bytedev
0

Предложить использовать NUnit чистый синтаксис делегата .

Пример для тестирования ArgumentNullExeption:

[Test]
[TestCase(null)]
public void FooCalculation_InvalidInput_ShouldThrowArgumentNullExeption(string text)
{
    var foo = new Foo();
    Assert.That(() => foo.Calculate(text), Throws.ArgumentNullExeption);

    //Or:
    Assert.That(() => foo.Calculate(text), Throws.Exception.TypeOf<ArgumentNullExeption>);
}
Шахар Шокрани
источник