O PHPUnit afirma que uma exceção foi lançada?

337

Alguém sabe se existe assertou algo assim que pode testar se uma exceção foi lançada no código que está sendo testado?

Felipe Almeida
fonte
2
Para essas respostas: e as multi-asserções em uma função de teste, e só espero ter uma exceção de lançamento? Tenho que separá-los e colocá-lo em uma função de teste independente?
Panwen Wang

Respostas:

549
<?php
require_once 'PHPUnit/Framework.php';

class ExceptionTest extends PHPUnit_Framework_TestCase
{
    public function testException()
    {
        $this->expectException(InvalidArgumentException::class);
        // or for PHPUnit < 5.2
        // $this->setExpectedException(InvalidArgumentException::class);

        //...and then add your test code that generates the exception 
        exampleMethod($anInvalidArgument);
    }
}

documentação do expectException () PHPUnit

O artigo do autor do PHPUnit fornece explicação detalhada sobre o teste das melhores práticas de exceções.

Frank Farmer
fonte
7
Se você usa espaços para nome, é necessário inserir o espaço para nome completo:$this->setExpectedException('\My\Name\Space\MyCustomException');
Alcalyn 25/11
15
O fato de você não poder designar a linha de código precisa que deve ser lançada é um erro IMO. E a incapacidade de testar mais de uma exceção no mesmo teste, torna o teste de muitas exceções esperadas um assunto realmente desajeitado. Eu escrevi uma afirmação real para tentar resolver esses problemas.
mindplay.dk
18
Para sua informação: a partir do phpunit 5.2.0, o setExpectedException método foi descontinuado e substituído pelo expectException. :)
hejdav
41
O que não é mencionado nos documentos ou aqui, mas o código esperado para lançar uma exceção precisa ser chamado depois expectException() . Embora possa ter sido óbvio para alguns, era uma pegadinha para mim.
21417 Jason Mclreary
7
Não é óbvio no documento, mas nenhum código após a sua função que lança uma exceção será executado. Portanto, se você quiser testar várias exceções no mesmo caso de teste, não poderá.
30517 laurent
122

Você também pode usar uma anotação de docblock até que o PHPUnit 9 seja lançado:

class ExceptionTest extends PHPUnit_Framework_TestCase
{
    /**
     * @expectedException InvalidArgumentException
     */
    public function testException()
    {
        ...
    }
}

Para o PHP 5.5+ (especialmente com código no namespace), agora eu prefiro usar ::class

David Harkness
fonte
3
IMO, este é o método preferido.
Mike Purcell
12
@LeviMorrison - IMHO a mensagem de exceção não deve ser testada, da mesma forma que as mensagens de log. Ambos são considerados informações úteis e estranhas ao executar análise forense manual . O ponto principal a ser testado é o tipo de exceção. Qualquer coisa além disso está vinculando muito fortemente à implementação. IncorrectPasswordExceptiondeve ser suficiente - para que a mensagem "Wrong password for [email protected]"seja igual a auxiliar. Acrescente a isso que você deseja gastar o menor tempo possível escrevendo testes e comece a ver a importância dos testes simples.
precisa saber é o seguinte
5
@DavidHarkness Imaginei que alguém traria isso à tona. Da mesma forma, eu concordaria que o teste de mensagens em geral é muito rigoroso e rígido. No entanto, é esse rigor e o vínculo estreito que podem (enfatizado propositalmente) ser o que se deseja em algumas situações, como a aplicação de uma especificação.
Levi Morrison
11
Eu não assistiria em um bloco de documentos para entender o que esperava, mas veria o código de teste real (independentemente do tipo de teste). Esse é o padrão para todos os outros testes; Não vejo razões válidas para exceções serem (oh deus) uma exceção a esta convenção.
Kamafeather
3
A regra "não testar a mensagem" parece válida, a menos que você teste um método que lança o mesmo tipo de exceção em várias partes do código, com a única diferença sendo o ID do erro, que é transmitido na mensagem. Seu sistema pode exibir uma mensagem para o usuário com base na mensagem de exceção (não no tipo de exceção). Nesse caso, importa qual mensagem o usuário vê; portanto, você deve testar a mensagem de erro.
21418 Vanja D.
34

Se você estiver executando o PHP 5.5+, poderá usar a ::classresolução para obter o nome da classe com expectException/setExpectedException . Isso fornece vários benefícios:

  • O nome será totalmente qualificado com seu espaço para nome (se houver).
  • Resolve para um stringpara que ele funcione com qualquer versão do PHPUnit.
  • Você obtém a conclusão de código no seu IDE.
  • O compilador PHP emitirá um erro se você digitar incorretamente o nome da classe.

Exemplo:

namespace \My\Cool\Package;

class AuthTest extends \PHPUnit_Framework_TestCase
{
    public function testLoginFailsForWrongPassword()
    {
        $this->expectException(WrongPasswordException::class);
        Auth::login('Bob', 'wrong');
    }
}

Compilações PHP

WrongPasswordException::class

para dentro

"\My\Cool\Package\WrongPasswordException"

sem PHPUnit ser o mais sábio.

Nota : O PHPUnit 5.2 foi introduzido expectException como um substituto para setExpectedException.

David Harkness
fonte
32

O código abaixo testará a mensagem de exceção e o código de exceção.

Importante: Ele falhará se a exceção esperada também não for lançada.

try{
    $test->methodWhichWillThrowException();//if this method not throw exception it must be fail too.
    $this->fail("Expected exception 1162011 not thrown");
}catch(MySpecificException $e){ //Not catching a generic Exception or the fail function is also catched
    $this->assertEquals(1162011, $e->getCode());
    $this->assertEquals("Exception Message", $e->getMessage());
}
Farid Movsumov
fonte
6
$this->fail()não é para ser usado dessa maneira, não acho, pelo menos não atualmente (PHPUnit 3.6.11); atua como uma exceção em si. Usando seu exemplo, se $this->fail("Expected exception not thrown")for chamado, o catchbloco será acionado e $e->getMessage()será "Exceção esperada não lançada" .
ken
11
@ken você provavelmente está certo. A chamada para failprovavelmente pertence após o bloco catch, não dentro da tentativa.
Frank fazendeiro
11
Eu tenho que reduzir o voto porque a chamada para failnão deve estar no trybloco. Ele, por si só, aciona o catchbloco, produzindo resultados falsos.
Twifty
6
Acredito que a razão pela qual isso não funcione bem é uma situação em que está capturando todas as exceções catch(Exception $e). Este método funciona muito bem para mim quando eu tento capturar exceções específicas:try { throw new MySpecificException; $this->fail('MySpecificException not thrown'); } catch(MySpecificException $e){}
spyle
23

Você pode usar a extensão assertException para afirmar mais de uma exceção durante a execução de um teste.

Insira o método no seu TestCase e use:

public function testSomething()
{
    $test = function() {
        // some code that has to throw an exception
    };
    $this->assertException( $test, 'InvalidArgumentException', 100, 'expected message' );
}

Eu também fiz uma característica para os amantes do bom código ..

hejdav
fonte
Qual PHPUnit você está usando? Estou usando o PHPUnit 4.7.5 e não assertExceptionestá definido. Também não consigo encontrá-lo no manual do PHPUnit.
physicalattraction
2
O asertExceptionmétodo não faz parte do PHPUnit original. Você deve herdar a PHPUnit_Framework_TestCaseclasse e adicionar o método vinculado na postagem acima manualmente. Seus casos de teste herdarão essa classe herdada.
hejdav
18

Uma maneira alternativa pode ser a seguinte:

$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Expected Exception Message');

Verifique se a sua classe de teste se estende \PHPUnit_Framework_TestCase.

Antonis Charalambous
fonte
Com certeza a mais de açúcar nesta sintaxe
AndrewMcLagan
13

O expectExceptionmétodo PHPUnit é muito inconveniente, pois permite testar apenas uma exceção por método de teste.

Eu fiz essa função auxiliar para afirmar que alguma função lança uma exceção:

/**
 * Asserts that the given callback throws the given exception.
 *
 * @param string $expectClass The name of the expected exception class
 * @param callable $callback A callback which should throw the exception
 */
protected function assertException(string $expectClass, callable $callback)
{
    try {
        $callback();
    } catch (\Throwable $exception) {
        $this->assertInstanceOf($expectClass, $exception, 'An invalid exception was thrown');
        return;
    }

    $this->fail('No exception was thrown');
}

Adicione-o à sua classe de teste e chame desta maneira:

public function testSomething() {
    $this->assertException(\PDOException::class, function() {
        new \PDO('bad:param');
    });
    $this->assertException(\PDOException::class, function() {
        new \PDO('foo:bar');
    });
}
Finesse
fonte
Definitivamente, a melhor solução de todas as respostas! Jogue-o em uma característica e empacote-o!
domdambrogia
11

Solução abrangente

As " melhores práticas " atuais do PHPUnit para testes de exceção parecem .. sem brilho ( docs ).

Como eu queria mais do que a expectExceptionimplementação atual , criei uma característica para usar nos meus casos de teste. São apenas ~ 50 linhas de código .

  • Suporta várias exceções por teste
  • Suporta asserções chamadas após a exceção ser lançada
  • Exemplos de uso claros e robustos
  • assertSintaxe padrão
  • Suporta asserções para mais do que apenas mensagem, código e classe
  • Suporta asserção inversa, assertNotThrows
  • Suporta Throwableerros do PHP 7

Biblioteca

Publiquei o AssertThrowstraço no Github e no packagist para que ele possa ser instalado com o compositor.

Exemplo Simples

Apenas para ilustrar o espírito por trás da sintaxe:

<?php

// Using simple callback
$this->assertThrows(MyException::class, [$obj, 'doSomethingBad']);

// Using anonymous function
$this->assertThrows(MyException::class, function() use ($obj) {
    $obj->doSomethingBad();
});

Muito arrumado?


Exemplo de uso completo

Veja abaixo um exemplo de uso mais abrangente:

<?php

declare(strict_types=1);

use Jchook\AssertThrows\AssertThrows;
use PHPUnit\Framework\TestCase;

// These are just for illustration
use MyNamespace\MyException;
use MyNamespace\MyObject;

final class MyTest extends TestCase
{
    use AssertThrows; // <--- adds the assertThrows method

    public function testMyObject()
    {
        $obj = new MyObject();

        // Test a basic exception is thrown
        $this->assertThrows(MyException::class, function() use ($obj) {
            $obj->doSomethingBad();
        });

        // Test custom aspects of a custom extension class
        $this->assertThrows(MyException::class, 
            function() use ($obj) {
                $obj->doSomethingBad();
            },
            function($exception) {
                $this->assertEquals('Expected value', $exception->getCustomThing());
                $this->assertEquals(123, $exception->getCode());
            }
        );

        // Test that a specific exception is NOT thrown
        $this->assertNotThrows(MyException::class, function() use ($obj) {
            $obj->doSomethingGood();
        });
    }
}

?>
jchook
fonte
4
Um pouco irônico que seu pacote para teste de unidade não inclua testes de unidade no repositório.
domdambrogia
2
@domdambrogia graças a @ jean-beguin , agora possui testes de unidade.
jchook
8
public function testException() {
    try {
        $this->methodThatThrowsException();
        $this->fail("Expected Exception has not been raised.");
    } catch (Exception $ex) {
        $this->assertEquals($ex->getMessage(), "Exception message");
    }

}
ab_wanyama
fonte
A assinatura de assertEquals()é assertEquals(mixed $expected, mixed $actual...), inverter como no seu exemplo, por isso deveria ser$this->assertEquals("Exception message", $ex->getMessage());
Roger Campanera
7

Aqui estão todas as asserções de exceção que você pode fazer. Observe que todos eles são opcionais .

class ExceptionTest extends PHPUnit_Framework_TestCase
{
    public function testException()
    {
        // make your exception assertions
        $this->expectException(InvalidArgumentException::class);
        // if you use namespaces:
        // $this->expectException('\Namespace\MyExceptio‌​n');
        $this->expectExceptionMessage('message');
        $this->expectExceptionMessageRegExp('/essage$/');
        $this->expectExceptionCode(123);
        // code that throws an exception
        throw new InvalidArgumentException('message', 123);
   }

   public function testAnotherException()
   {
        // repeat as needed
        $this->expectException(Exception::class);
        throw new Exception('Oh no!');
    }
}

A documentação pode ser encontrada aqui .

Westy92
fonte
Está incorreto porque o PHP para na primeira exceção lançada. O PHPUnit verifica se a exceção lançada tem o tipo correto e diz "o teste está bom", nem mesmo sabe sobre a segunda exceção.
quer
3
/**
 * @expectedException Exception
 * @expectedExceptionMessage Amount has to be bigger then 0!
 */
public function testDepositNegative()
{
    $this->account->deposit(-7);
}

Tenha muito cuidado "/**", observe o duplo "*". Escrever apenas "**" (asterix) falhará no seu código. Certifique-se também de usar a última versão do phpUnit. Em algumas versões anteriores do phpunit @expectedException Exception não é suportado. Eu tinha 4.0 e não funcionou para mim, tive que atualizar para 5.5 https://coderwall.com/p/mklvdw/install-phpunit-with-composer para atualizar com o compositor.

C Cislariu
fonte
0

Para o PHPUnit 5.7.27 e PHP 5.6 e para testar várias exceções em um teste, era importante forçar o teste de exceção. Usar o tratamento de exceções sozinho para afirmar a instância de Exceção ignorará o teste da situação se nenhuma exceção ocorrer.

public function testSomeFunction() {

    $e=null;
    $targetClassObj= new TargetClass();
    try {
        $targetClassObj->doSomething();
    } catch ( \Exception $e ) {
    }
    $this->assertInstanceOf(\Exception::class,$e);
    $this->assertEquals('Some message',$e->getMessage());

    $e=null;
    try {
        $targetClassObj->doSomethingElse();
    } catch ( Exception $e ) {
    }
    $this->assertInstanceOf(\Exception::class,$e);
    $this->assertEquals('Another message',$e->getMessage());

}
ácido
fonte
0
function yourfunction($a,$z){
   if($a<$z){ throw new <YOUR_EXCEPTION>; }
}

aqui está o teste

class FunctionTest extends \PHPUnit_Framework_TestCase{

   public function testException(){

      $this->setExpectedException(<YOUR_EXCEPTION>::class);
      yourfunction(1,2);//add vars that cause the exception 

   }

}
sami klah
fonte
0

PhpUnit é uma biblioteca incrível, mas esse ponto específico é um pouco frustrante. É por isso que podemos usar a biblioteca opensource turbotesting-php, que possui um método de asserção muito conveniente para nos ajudar a testar exceções. Pode ser encontrada aqui:

https://github.com/edertone/TurboTesting/blob/master/TurboTesting-Php/src/main/php/utils/AssertUtils.php

E, para usá-lo, basta fazer o seguinte:

AssertUtils::throwsException(function(){

    // Some code that must throw an exception here

}, '/expected error message/');

Se o código digitado dentro da função anônima não gerar uma exceção, uma exceção será lançada.

Se o código que digitarmos dentro da função anônima gerar uma exceção, mas sua mensagem não corresponder ao regexp esperado, uma exceção também será lançada.

Jaume Mussons Abad
fonte