A melhor maneira de testar exceções com Assert para garantir que serão lançadas

97

Você acha que esta é uma boa maneira de testar exceções? Alguma sugestão?

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

Assert.IsNotNull(exception);

Estou usando o MS Test.

Hannoun Yassir
fonte

Respostas:

137

Eu tenho alguns padrões diferentes que uso. Eu uso o ExpectedExceptionatributo na maioria das vezes quando uma exceção é esperada. Isso é suficiente para a maioria dos casos, no entanto, há alguns casos em que isso não é suficiente. A exceção pode não ser capturável - já que é lançada por um método que é invocado por reflexão - ou talvez eu apenas queira verificar se outras condições se mantêm, digamos que uma transação seja revertida ou algum valor ainda tenha sido definido. Nesses casos, eu o envolvo em um try/catchbloco que espera a exceção exata, faz um Assert.Failse o código for bem-sucedido e também captura exceções genéricas para garantir que uma exceção diferente não seja lançada.

Primeiro caso:

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

Segundo caso:

[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
fonte
16
Muitas estruturas de teste de unidade implementam falhas de asserção como exceções. Portanto, o Assert.Fail () no segundo caso será capturado pelo bloco catch (Exception), que ocultará a mensagem de exceção. Você precisa adicionar um catch (NUnit.Framework.AssertionException) {throw;} ou similar - veja minha resposta.
GrahamS
@ Graham - eu digitei isso no topo da minha cabeça. Normalmente, eu também imprimiria a mensagem de exceção, além de seu tipo. A questão é que o teste falharia, pois o segundo manipulador detectaria a falha de asserção e "voltaria a falhar" com informações sobre o erro.
tvanfosson,
1
Embora seu código seja funcionalmente sólido, não recomendo usar o atributo ExpectedException (já que é muito restritivo e sujeito a erros) ou escrever um bloco try / catch em cada teste (já que é muito complicado e sujeito a erros). Use um método de afirmação bem projetado - fornecido por sua estrutura de teste ou escreva o seu próprio. Você pode obter um código melhor e não terá que escolher entre as diferentes técnicas ou mudar de uma para outra conforme o teste muda. Consulte stackoverflow.com/a/25084462/2166177
steve
Para sua informação - mudei para o uso do xUnit, que possui um Assert.Throwsmétodo fortemente tipado que cobre ambos os casos.
tvanfosson
O atributo ExpectedException é uma forma desagradável e datada de testar se as exceções são lançadas. Veja minha resposta completa abaixo.
bytedev
42

Agora, 2017, você pode fazer isso mais facilmente com a nova estrutura MSTest V2 :

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

//async version
await Assert.ThrowsExceptionAsync<SomeException>(
  () => myObject.SomeMethodAsync()
);
Icaro Bombonato
fonte
Isso só terá sucesso se um System.Exceptionfor lançado. Qualquer outro, como um, System.ArgumentExceptionserá reprovado no teste.
sschoof de
2
Se você está esperando outro tipo de exceção, deve testá-lo ... Em seu exemplo, você deve fazer: Assert.ThrowsException <ArgumentException> (() => myClass.MyMethodWithError ());
Icaro Bombonato de
2
Algo importante a ser observado é que o uso de Assert.ThrowsException<MyException>testará somente contra o tipo de exceção fornecido, e não qualquer um de seus tipos de exceção derivados. No meu exemplo, se o testado Subfosse para Throwa MyInheritedException(tipo derivado da classe base MyException), o teste falharia .
Ama,
Se você deseja ampliar seu teste e aceitar um tipo de exceção, bem como seus tipos derivados, use a Try { SubToTest(); Assert.Fail("...") } Catch (AssertFailedException e) {throw;} Catch (MyException e) {...}. Observe a importância Catch (AssertFailedException e) {throw;}máxima de (cf. comentário de allgeek)
Ama
17

Sou novo aqui e não tenho reputação para comentar ou votar negativamente, mas gostaria de apontar uma falha no exemplo na resposta de Andy White :

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

Em todas as estruturas de teste de unidade com as quais estou familiarizado, Assert.Failfunciona lançando uma exceção, de modo que a captura genérica na verdade mascarará a falha do teste. Se SomethingThatCausesAnException()não jogar, o Assert.Failfará, mas isso nunca borbulhará para o executor de teste para indicar falha.

Se você precisar capturar a exceção esperada (ou seja, para declarar certos detalhes, como a mensagem / propriedades na exceção), é importante capturar o tipo esperado específico, e não a classe de exceção base. Isso permitiria que a Assert.Failexceção borbulhasse (assumindo que você não está lançando o mesmo tipo de exceção que sua estrutura de teste de unidade faz), mas ainda permitiria a validação na exceção que foi lançada por seu SomethingThatCausesAnException()método.

Allgeek
fonte
15

A partir da versão 2.5, o NUnit tem os seguintes níveis de método Assertpara testar exceções:

Assert.Throws , que testará um tipo de exceção exato:

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

E Assert.Catch, que testará para uma exceção de um determinado tipo, ou um tipo de exceção derivado deste tipo:

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

Como um aparte, ao depurar testes de unidade que lançam exceções, você pode querer evitar que o VS interrompa a exceção .

Editar

Apenas para dar um exemplo do comentário de Mateus abaixo, o retorno do genérico Assert.Throwse Assert.Catché a exceção com o tipo da exceção, que você pode examinar para uma inspeção mais detalhada:

// The type of ex is that of the generic type parameter (SqlException)
var ex = Assert.Throws<SqlException>(() => MethodWhichDeadlocks());
Assert.AreEqual(1205, ex.Number);
StuartLC
fonte
2
Roy Osherove recomenda isso em The Art of Unit Testing, segunda edição, seção 2.6.2.
Avi
2
Eu gosto Assert.Throws, além disso, ele retorna a exceção para que você possa escrever mais afirmações sobre a própria exceção.
Matthew
A pergunta era para o MSTest, não para o NUnit.
bytedev
A pergunta original de @nashwan OP não tinha essa qualificação, e a marcação ainda não qualifica o MS-Test. Do jeito que está, é uma questão de teste de unidade C #, .Net.
StuartLC de
11

Infelizmente, o MSTest AINDA só tem realmente o atributo ExpectedException (apenas mostra o quanto a MS se preocupa com o MSTest), que é muito ruim porque quebra o padrão Arrange / Act / Assert e não permite que você especifique exatamente qual linha de código você espera da exceção para ocorrer.

Quando estou usando (/ forçado por um cliente) para usar MSTest, sempre uso esta classe auxiliar:

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

Exemplo de uso:

AssertException.Throws<ArgumentNullException>(() => classUnderTest.GetCustomer(null));
bytedev
fonte
10

Como alternativa ao uso de ExpectedExceptionatributo, às vezes defino dois métodos úteis para minhas classes de teste:

AssertThrowsException() recebe um delegado e afirma que ele lança a exceção esperada com a mensagem esperada.

AssertDoesNotThrowException() pega o mesmo delegado e afirma que não lança uma exceção.

Esse emparelhamento pode ser muito útil quando você deseja testar se uma exceção é lançada em um caso, mas não no outro.

Com eles, meu código de teste de unidade pode ter a seguinte aparência:

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

Legal e legal hein?

Os métodos My AssertThrowsException()e AssertDoesNotThrowException()são definidos em uma classe base comum da seguinte maneira:

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
fonte
4

Marque o teste com ExpectedExceptionAttribute (este é o termo em NUnit ou MSTest; usuários de outras estruturas de teste de unidade podem precisar traduzir).

Itowlson
fonte
Não use ExpectedExceptionAttribute (motivo dado em minha postagem abaixo). NUnit tem Assert.Throws <YourException> () e para MSTest use algo como minha classe AssertException abaixo.
bytedev
3

Com a maioria das estruturas de teste de unidade .net, você pode colocar um atributo [ExpectedException] no método de teste. No entanto, isso não pode dizer que a exceção aconteceu no ponto que você esperava. É aí que a xunit.net pode ajudar.

Com o xunit você tem Assert.Throws, então você pode fazer coisas assim:

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

[Fato] é o equivalente xunit de [TestMethod]

Steve Willcock
fonte
Se você deve usar o MSTest (que sou forçado a usar frequentemente pelos empregadores), veja minha resposta abaixo.
bytedev
0

Sugira o uso da sintaxe de delegado limpo do NUnit .

Exemplo para teste 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>);
}
Shahar Shokrani
fonte