Como uso o Assert para verificar se uma exceção foi lançada?

830

Como uso Assert(ou outra classe de teste?) Para verificar se uma exceção foi lançada?

Alex
fonte
Qual estrutura de teste de unidade você está usando?
Kevin Pullin
3
Visual Studio Integrated
Alex
4
O atributo ExpectedException não ajuda? ref: msdn.microsoft.com/en-us/library/...
shahkalpesh
2
Engraçado, acabei de procurar a resposta, encontrei-a em stackoverflow.com/questions/741029/testing-exceptions .
Dfjacobs 01/06/09
Consulte também: stackoverflow.com/questions/741029/…
bytedev

Respostas:

978

Para "Teste da equipe do Visual Studio", parece que você aplica o atributo ExpectedException ao método do teste.

Exemplo da documentação aqui: Um passo a passo de teste de unidade com o Visual Studio Team Test

[TestMethod]
[ExpectedException(typeof(ArgumentException),
    "A userId of null was inappropriately allowed.")]
public void NullUserIdInConstructor()
{
   LogonInfo logonInfo = new LogonInfo(null, "P@ss0word");
}
Kevin Pullin
fonte
25
O atributo ExpectedException acima também funciona no NUnit (mas [TestMethod] deve ser [Test]).
Dbkk 01/06/09
5
@dbkk: não funciona exatamente o mesmo em NUnit - a mensagem é tratada como uma cadeia que precisa matcvh a mensagem de exceção (e IU acho que faz mais sentido)
Ruben Bartelink
29
Esse atributo faz o trabalho e é um recurso interno para programadores de c #, mas não recomendo usá-lo, pois não é flexível o suficiente. Considere o que acontece se o tipo de exceção for lançado pelo seu código de configuração de teste: o teste passa, mas não fez o que você esperava. Ou, se você quiser testar o estado do objeto de exceção. Normalmente, quero usar StringAssert.Contains (e.Message ...) em vez de testar a mensagem inteira. Use um método de afirmação, conforme descrito em outras respostas.
steve
3
Evite usar ExpectedException no NUnit, pois ele será descartado no NUnit 3.0. Eu prefiro usar Assert.Throws <SpecificException> ()
Terence
5
Você pode usar Assert.ThrowsException <T> e Assert.ThrowsExceptionAsync <T> dentro de MsTest.
Gopal Krishnan
257

Normalmente, sua estrutura de teste terá uma resposta para isso. Mas se não for flexível o suficiente, você sempre poderá fazer isso:

try {
    somethingThatShouldThrowAnException();
    Assert.Fail(); // If it gets to this line, no exception was thrown
} catch (GoodException) { }

Como @Jonas aponta, isso NÃO funciona para capturar uma exceção de base:

try {
    somethingThatShouldThrowAnException();
    Assert.Fail(); // raises AssertionException
} catch (Exception) {
    // Catches the assertion exception, and the test passes
}

Se você absolutamente precisar capturar Exception, será necessário repetir novamente o Assert.Fail (). Mas, realmente, este é um sinal de que você não deveria escrever isso à mão; verifique se há opções na estrutura de teste ou veja se você pode lançar uma exceção mais significativa para testar.

catch (AssertionException) { throw; }

Você poderá adaptar essa abordagem ao que quiser - incluindo a especificação de quais tipos de exceções serão capturadas. Se você espera apenas determinados tipos, finalize os catchblocos com:

} catch (GoodException) {
} catch (Exception) {
    // not the right kind of exception
    Assert.Fail();
}
ojrac
fonte
20
+1, uso dessa maneira em vez do atributo quando preciso fazer afirmações além do tipo de exceção. Por exemplo, e se for necessário verificar se determinados campos na instância de exceção estão definidos com certos valores.
Pavel Repin
2
Você não precisa especificar a mensagem de erro. Isso é suficiente: [ExpectedException (typeof (ArgumentException))]]
mibollma
5
Eu acho que essa solução é a melhor. [ExpectedException (typeof (ArgumentException))] tem seus usos, se o teste for simples, mas, no meu ponto de vista, é uma solução preguiçosa e estar confortável pode levar a armadilhas. Essa solução fornece controle específico para você fazer um teste mais correto, além de poder fazer um Writeline de teste no relatório de execução de teste, de que a exceção foi realmente lançada conforme o esperado.
evilfish
12
Tenha cuidado com isso, porque Assert.Fail () gera uma exceção, se você a captura, o teste passa!
Jonas
4
@ Vinnyq12 O que quero dizer é que o primeiro teste no exemplo acima nunca falhará. Um teste falhar se uma exceção é lançada (e não "pegar" pela ExpectedExceptionAttribute)
Jonas
113

Meu método preferido para implementar isso é escrever um método chamado Throws e usá-lo como qualquer outro método Assert. Infelizmente, o .NET não permite que você escreva um método de extensão estática; portanto, você não pode usá-lo como se realmente pertencesse à compilação na classe Assert; basta criar outro chamado MyAssert ou algo semelhante. A classe fica assim:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace YourProject.Tests
{
    public static class MyAssert
    {
        public static void Throws<T>( Action func ) where T : Exception
        {
            var exceptionThrown = false;
            try
            {
                func.Invoke();
            }
            catch ( T )
            {
                exceptionThrown = true;
            }

            if ( !exceptionThrown )
            {
                throw new AssertFailedException(
                    String.Format("An exception of type {0} was expected, but not thrown", typeof(T))
                    );
            }
        }
    }
}

Isso significa que seu teste de unidade se parece com isso:

[TestMethod()]
public void ExceptionTest()
{
    String testStr = null;
    MyAssert.Throws<NullReferenceException>(() => testStr.ToUpper());
}

Que se parece e se comporta muito mais com o resto das suas sintaxes de teste de unidade.

Richiban
fonte
1
Livre-se do sinalizador bool e coloque o arremesso em linha diretamente após a chamada para uma implementação mais compacta.
gt
11
A única coisa que melhora isso é fazer com que a função retorne a exceção capturada, para que você possa continuar afirmando que coisas como os atributos na exceção estão corretas.
Mark Hildreth
2
Obrigado! Essa parece ser a melhor abordagem para mim, porque é uma maneira curta de testar várias exceções em um método. Também é muito mais legível.
David Sherret
2
Os atributos @MickeyPerlstein quebram as regras AAA para teste. Especificamente, se o seu Organizar lançar a exceção antes mesmo de você chegar à lei, seu teste será aprovado ... eek!
freedomn-m
2
A Microsoft finalmente conseguiu atualizar o MSTest - v2 suporta Assert.ThrowsException<T>e Assert.ThrowsExceptionAsync<T>- consulte blogs.msdn.microsoft.com/visualstudioalm/2017/02/25/…
Quango
62

se você usa NUNIT, pode fazer algo assim:

Assert.Throws<ExpectedException>(() => methodToTest());


Também é possível armazenar a exceção lançada para validá-la ainda mais:

ExpectedException ex = Assert.Throws<ExpectedException>(() => methodToTest());
Assert.AreEqual( "Expected message text.", ex.Message );
Assert.AreEqual( 5, ex.SomeNumber);

Consulte: http://nunit.org/docs/2.5/exceptionAsserts.html

damir
fonte
60

Se você estiver usando o MSTest, que originalmente não tinha um ExpectedExceptionatributo, você pode fazer o seguinte:

try 
{
    SomeExceptionThrowingMethod()
    Assert.Fail("no exception thrown");
}
catch (Exception ex)
{
    Assert.IsTrue(ex is SpecificExceptionType);
}
Jon Limjap
fonte
3
Isso funciona, mas eu não recomendo isso em geral, pois a lógica é excessivamente complicada. Não estou dizendo que é complicado, mas considere se você escrever esse bloco de código para vários testes - 10s, 100s de testes. Essa lógica precisa ser cultivada em um método de afirmação bem projetado. Veja outras respostas.
steve
35

Desconfie de usar ExpectedException, pois isso pode levar a várias armadilhas, conforme demonstrado aqui:

http://geekswithblogs.net/sdorman/archive/2009/01/17/unit-testing-and-expected-exceptions.aspx

E aqui:

http://xunit.github.io/docs/comparisons.html

Se você precisar testar exceções, haverá menos maneiras desaprovadas. Você pode usar o método try {act / fail} catch {assert}, que pode ser útil para estruturas que não têm suporte direto para testes de exceção diferentes de ExpectedException.

Uma alternativa melhor é usar o xUnit.NET, que é uma estrutura de teste de unidade muito moderna, voltada para o futuro e extensível, que aprendeu com todos os outros erros e melhorou. Um desses aprimoramentos é o Assert.Throws, que fornece uma sintaxe muito melhor para a declaração de exceções.

Você pode encontrar o xUnit.NET no github: http://xunit.github.io/

jrista
fonte
4
Note-se que NUnit 2.5 também suporta a sintaxe estilo Assert.Throws agora também - nunit.com/index.php?p=releaseNotes&r=2.5
Alconja
A maneira como os testes de unidade param para informar você sobre a exceção ao usar ExpectedException me deixa louco. Por que a MS achou uma boa idéia ter uma etapa manual nos testes automatizados? Obrigado pelos links.
Ant
@ Ant: A MS copiou o NUnit ... então a verdadeira questão é: por que o NUnit achava que era uma boa idéia?
jrista
28

O MSTest (v2) agora tem uma função Assert.ThrowsException que pode ser usada assim:

Assert.ThrowsException<System.FormatException>(() =>
            {
                Story actual = PersonalSite.Services.Content.ExtractHeader(String.Empty);
            }); 

Você pode instalá-lo com nuget: Install-Package MSTest.TestFramework

Martin Beeby
fonte
Em 2018, essa é considerada a melhor prática, pois verifica apenas a unidade em teste e não outro código.
CM
24

Em um projeto em que estou trabalhando, temos outra solução para isso.

Primeiro, não gosto do ExpectedExceptionAttribute, pois leva em consideração qual chamada de método causou a exceção.

Eu faço isso com um método auxiliar em seu lugar.

Teste

[TestMethod]
public void AccountRepository_ThrowsExceptionIfFileisCorrupt()
{
     var file = File.Create("Accounts.bin");
     file.WriteByte(1);
     file.Close();

     IAccountRepository repo = new FileAccountRepository();
     TestHelpers.AssertThrows<SerializationException>(()=>repo.GetAll());            
}

HelperMethod

public static TException AssertThrows<TException>(Action action) where TException : Exception
    {
        try
        {
            action();
        }
        catch (TException ex)
        {
            return ex;
        }
        Assert.Fail("Expected exception was not thrown");

        return null;
    }

Legal, não é;)

Glenn
fonte
14

É um atributo no método de teste ... você não usa Assert. Se parece com isso:

[ExpectedException(typeof(ExceptionType))]
public void YourMethod_should_throw_exception()
bytebender
fonte
13

Você pode baixar um pacote do Nuget usando: PM> Install-Package MSTestExtensions que adiciona a sintaxe Assert.Throws () no estilo de nUnit / xUnit ao MsTest.

Instruções de alto nível: baixe o assembly e herde do BaseTest e você pode usar a sintaxe Assert.Throws () .

O método principal para a implementação Throws é o seguinte:

public static void Throws<T>(Action task, string expectedMessage, ExceptionMessageCompareOptions options) where T : Exception
{
    try
    {
        task();
    }
    catch (Exception ex)
    {
        AssertExceptionType<T>(ex);
        AssertExceptionMessage(ex, expectedMessage, options);
        return;
    }

    if (typeof(T).Equals(new Exception().GetType()))
    {
        Assert.Fail("Expected exception but no exception was thrown.");
    }
    else
    {
        Assert.Fail(string.Format("Expected exception of type {0} but no exception was thrown.", typeof(T)));
    }
}

Divulgação: Eu montei este pacote.

Mais informações: http://www.bradoncode.com/blog/2012/01/asserting-exceptions-in-mstest-with.html

Bradley Braithwaite
fonte
Obrigado pelo exemplo. Você tem um exemplo de como testar um Assert.DoesNotThrow () ou equivalente?
Pista Goolsby
10

Você pode conseguir isso com uma linha simples.

Se sua operação foo.bar()é assíncrona:

await Assert.ThrowsExceptionAsync<Exception>(() => foo.bar());

Se foo.bar()não for assíncrono

Assert.ThrowsException<Exception>(() => foo.bar());
Cfrim
fonte
1
Existem muitas outras respostas. Para mim, eu estava procurando uma maneira abreviada de testar condições de falha conhecidas apenas pelo Tipo de Exceção, o que facilita os casos de teste legíveis mais fáceis. NOTA: o tipo de exceção não corresponde às classes de exceções herdadas, como um try-catch padrão; portanto, o exemplo acima não interceptará um, ArgumentExceptionpor exemplo. A antiga tentativa de captura e teste da resposta de exceção ainda é preferida se você tiver critérios avançados para testar, mas, para muitos dos meus casos, isso ajuda muito!
21419 Chris Schaller
5

Não recomendo usar o atributo ExpectedException (já que é muito restritivo e propenso a erros) ou escrever um bloco try / catch em cada teste (já que é muito complicado e propenso a erros). Use um método de afirmação bem projetado - fornecido pela sua estrutura de teste ou escreva seu próprio. Aqui está o que eu escrevi e uso.

public static class ExceptionAssert
{
    private static T GetException<T>(Action action, string message="") where T : Exception
    {
        try
        {
            action();
        }
        catch (T exception)
        {
            return exception;
        }
        throw new AssertFailedException("Expected exception " + typeof(T).FullName + ", but none was propagated.  " + message);
    }

    public static void Propagates<T>(Action action) where T : Exception
    {
        Propagates<T>(action, "");
    }

    public static void Propagates<T>(Action action, string message) where T : Exception
    {
        GetException<T>(action, message);
    }

    public static void Propagates<T>(Action action, Action<T> validation) where T : Exception
    {
        Propagates(action, validation, "");
    }

    public static void Propagates<T>(Action action, Action<T> validation, string message) where T : Exception
    {
        validation(GetException<T>(action, message));
    }
}

Exemplo usa:

    [TestMethod]
    public void Run_PropagatesWin32Exception_ForInvalidExeFile()
    {
        (test setup that might propagate Win32Exception)
        ExceptionAssert.Propagates<Win32Exception>(
            () => CommandExecutionUtil.Run(Assembly.GetExecutingAssembly().Location, new string[0]));
        (more asserts or something)
    }

    [TestMethod]
    public void Run_PropagatesFileNotFoundException_ForExecutableNotFound()
    {
        (test setup that might propagate FileNotFoundException)
        ExceptionAssert.Propagates<FileNotFoundException>(
            () => CommandExecutionUtil.Run("NotThere.exe", new string[0]),
            e => StringAssert.Contains(e.Message, "NotThere.exe"));
        (more asserts or something)
    }

NOTAS

Retornar a exceção em vez de oferecer suporte a um retorno de chamada de validação é uma idéia razoável, exceto que isso torna a sintaxe de chamada dessa declaração muito diferente das outras que eu uso.

Diferentemente de outros, eu uso 'propaga' em vez de 'lança', pois só podemos testar se uma exceção se propaga a partir de uma chamada. Não podemos testar diretamente se uma exceção é lançada. Mas suponho que você possa imaginar lances como significados: jogados e não capturados.

PENSAMENTO FINAL

Antes de mudar para esse tipo de abordagem, considerei usar o atributo ExpectedException quando um teste verificou apenas o tipo de exceção e usar um bloco try / catch se mais validação fosse necessária. Mas não apenas precisaria pensar em qual técnica usar para cada teste, mas alterar o código de uma técnica para outra conforme as necessidades mudassem não era um esforço trivial. Usar uma abordagem consistente economiza esforço mental.

Então, em resumo, essa abordagem é esportiva: facilidade de uso, flexibilidade e robustez (difícil de fazer errado).

Steve
fonte
4

O auxiliar fornecido pelo @Richiban acima funciona muito bem, exceto que não lida com a situação em que uma exceção é lançada, mas não com o tipo esperado. Os seguintes endereços que:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace YourProject.Tests
{
    public static class MyAssert
    {
        /// <summary>
        /// Helper for Asserting that a function throws an exception of a particular type.
        /// </summary>
        public static void Throws<T>( Action func ) where T : Exception
        {
            Exception exceptionOther = null;
            var exceptionThrown = false;
            try
            {
                func.Invoke();
            }
            catch ( T )
            {
                exceptionThrown = true;
            }
            catch (Exception e) {
                exceptionOther = e;
            }

            if ( !exceptionThrown )
            {
                if (exceptionOther != null) {
                    throw new AssertFailedException(
                        String.Format("An exception of type {0} was expected, but not thrown. Instead, an exception of type {1} was thrown.", typeof(T), exceptionOther.GetType()),
                        exceptionOther
                        );
                }

                throw new AssertFailedException(
                    String.Format("An exception of type {0} was expected, but no exception was thrown.", typeof(T))
                    );
            }
        }
    }
}
Martin Connell
fonte
2
Hmmm ... eu entendo a idéia, mas não tenho certeza se concordo que é melhor. Só porque queremos garantir que uma exceção específica seja levantada não significa que todas as outras devem ser agrupadas como uma falha de afirmação. IMHO uma exceção desconhecida deve apenas aumentar a pilha como faria em qualquer outra operação de afirmação.
Crono
@ Martin eu remover o código envolvendo exceptionOther e simplesmente relançar a partir da segunda cláusula catch
Tom Lint
4

Como você menciona o uso de outras classes de teste, uma opção melhor que o ExpectedExceptionatributo é usar o Should.Throw de Shoudly .

Should.Throw<DivideByZeroException>(() => { MyDivideMethod(1, 0); });

Digamos que tenhamos um requisito de que o cliente precise de um endereço para criar um pedido . Caso contrário, o CreateOrderForCustomermétodo deve resultar em um ArgumentException. Então poderíamos escrever:

[TestMethod]
public void NullUserIdInConstructor()
{
  var customer = new Customer(name := "Justin", address := null};

  Should.Throw<ArgumentException>(() => {
    var order = CreateOrderForCustomer(customer) });
}

Isso é melhor do que usar um ExpectedException atributo, porque estamos sendo específicos sobre o que deve gerar o erro. Isso torna os requisitos de nossos testes mais claros e também facilita o diagnóstico quando o teste falha.

Observe que também existe um Should.ThrowAsyncteste de método assíncrono.

Justin J Stark
fonte
4

Como alternativa, você pode tentar testar as exceções, de fato, são lançadas com as próximas 2 linhas em seu teste.

var testDelegate = () => MyService.Method(params);
Assert.Throws<Exception>(testDelegate);
Matias
fonte
4

Nos testes de unidade integrados do VS, se você simplesmente deseja verificar se "qualquer exceção" é lançada, mas não sabe o tipo, pode usar um catch all:

[TestMethod]
[ExpectedException(typeof(Exception), AllowDerivedTypes = true)]
public void ThrowExceptionTest()
{
    //...
}
TTT
fonte
3

Bem, vou resumir o que todo mundo aqui disse antes ... De qualquer forma, aqui está o código que construí de acordo com as boas respostas :) Tudo o que resta fazer é copiar e usar ...

/// <summary>
/// Checks to make sure that the input delegate throws a exception of type TException.
/// </summary>
/// <typeparam name="TException">The type of exception expected.</typeparam>
/// <param name="methodToExecute">The method to execute to generate the exception.</param>
public static void AssertRaises<TException>(Action methodToExecute) where TException : System.Exception
{
    try
    {
        methodToExecute();
    }
    catch (TException) {
        return;
    }  
    catch (System.Exception ex)
    {
        Assert.Fail("Expected exception of type " + typeof(TException) + " but type of " + ex.GetType() + " was thrown instead.");
    }
    Assert.Fail("Expected exception of type " + typeof(TException) + " but no exception was thrown.");  
}
roeiba
fonte
2

Confira o nUnit Docs para exemplos sobre:

[ExpectedException( typeof( ArgumentException ) )]
Jon Masters
fonte
2

Isso vai depender de qual estrutura de teste você está usando?

No MbUnit, por exemplo, você pode especificar a exceção esperada com um atributo para garantir que você esteja recebendo a exceção que realmente espera.

[ExpectedException(typeof(ArgumentException))]
Jay
fonte
2

No caso de usar o NUnit , tente o seguinte:

Assert.That(() =>
        {
            Your_Method_To_Test();
        }, Throws.TypeOf<Your_Specific_Exception>().With.Message.EqualTo("Your_Specific_Message"));
Amir Chatrbahr
fonte
2

Existe uma biblioteca incrível chamada NFluent que acelera e facilita a maneira como você escreve suas afirmações .

É bem simples escrever uma afirmação para lançar uma exceção:

    [Test]
    public void given_when_then()
    {
        Check.ThatCode(() => MethodToTest())
            .Throws<Exception>()
            .WithMessage("Process has been failed");
    }
mirind4
fonte
1

Embora essa seja uma pergunta antiga, eu gostaria de acrescentar um novo pensamento à discussão. Eu estendi o padrão Organizar, Agir, Assertar para ser Esperado, Organizar, Agir, Assertar. Você pode criar um ponteiro de exceção esperado e afirmar que ele foi atribuído. Isso parece mais limpo do que fazer seus Asserts em um bloco catch, deixando a seção Act principalmente apenas para uma linha de código chamar o método em teste. Você também não precisa de Assert.Fail();ou returnpara vários pontos no código. Qualquer outra exceção lançada fará com que o teste falhe, porque não será capturado e, se uma exceção do seu tipo esperado for lançada, mas não for a que você estava esperando, afirmando contra a mensagem ou outras propriedades de a exceção ajuda a garantir que seu teste não seja aprovado inadvertidamente.

[TestMethod]
public void Bar_InvalidDependency_ThrowsInvalidOperationException()
{
    // Expectations
    InvalidOperationException expectedException = null;
    string expectedExceptionMessage = "Bar did something invalid.";

    // Arrange
    IDependency dependency = DependencyMocks.Create();
    Foo foo = new Foo(dependency);

    // Act
    try
    {
        foo.Bar();
    }
    catch (InvalidOperationException ex)
    {
        expectedException = ex;
    }

    // Assert
    Assert.IsNotNull(expectedException);
    Assert.AreEqual(expectedExceptionMessage, expectedException.Message);
}
Adam Venezia
fonte