PHPUnit affirme qu'une exception a été levée?


338

Est-ce que quelqu'un sait s'il existe un assertou quelque chose comme ça qui peut tester si une exception a été levée dans le code testé?


2
À ces réponses: qu'en est-il des multi-assertions dans une fonction de test, et je m'attends juste à avoir une exception de levée? Dois-je les séparer et mettre celui-ci dans une fonction de test indépendante?
Panwen Wang

Réponses:


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

expectException () documentation PHPUnit

L'article de l'auteur PHPUnit fournit des explications détaillées sur les meilleures pratiques de test des exceptions.


8
Si vous utilisez des espaces de noms, vous devez bien saisir l'espace de noms complet:$this->setExpectedException('\My\Name\Space\MyCustomException');
Alcalyn

15
Le fait que vous ne pouvez pas désigner la ligne de code précise qui doit être lancée est une erreur OMI. Et l'incapacité de tester plus d'une exception dans le même test, rend le test de nombreuses exceptions attendues une affaire vraiment maladroite. J'ai écrit une affirmation réelle pour essayer de résoudre ces problèmes.
mindplay.dk

18
Pour info: à partir de phpunit 5.2.0, la setExpectedException méthode est déconseillée, remplacée par celle- expectExceptionlà. :)
hejdav

41
Ce qui n'est pas mentionné dans la documentation ou ici, mais le code censé lever une exception doit être appelé après expectException() . Bien que cela ait pu être évident pour certains, c'était un piège pour moi.
Jason McCreary

7
Ce n'est pas évident d'après la doc, mais aucun code après votre fonction qui lève une exception ne sera exécuté. Donc, si vous souhaitez tester plusieurs exceptions dans le même scénario de test, vous ne pouvez pas.
laurent

122

Vous pouvez également utiliser une annotation docblock jusqu'à la sortie de PHPUnit 9:

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

Pour PHP 5.5+ (en particulier avec le code avec espace de noms), je préfère maintenant utiliser ::class


3
OMI, c'est la méthode préférée.
Mike Purcell

12
@LeviMorrison - À mon humble avis, le message d' exception ne doit pas être testé, de la même manière que les messages de journal. Les deux sont considérés comme des informations inutiles et utiles lors de la criminalistique manuelle . Le point clé à tester est le type d'exception. Tout ce qui va au-delà lie trop étroitement la mise en œuvre. IncorrectPasswordExceptiondevrait être suffisant - que le message égal "Wrong password for bob@me.com"est accessoire. Ajoutez à cela que vous voulez passer le moins de temps possible à écrire des tests, et vous commencez à voir à quel point les tests simples deviennent importants.
David Harkness

5
@DavidHarkness J'ai pensé que quelqu'un pourrait en parler. De même, je conviens que le test des messages en général est trop strict et serré. Cependant, c'est cette rigueur et cette liaison stricte qui peuvent (soulignées à dessein) être ce que l'on souhaite dans certaines situations, telles que l'application d'une spécification.
Levi Morrison

1
Je ne regarderais pas dans un doc-block pour comprendre ce qu'il attendait, mais je regarderais le code de test réel (quel que soit le type de test). C'est la norme pour tous les autres tests; Je ne vois pas de raisons valables pour que les exceptions soient (oh mon dieu) une exception à cette convention.
Kamafeather

3
La règle "ne pas tester le message" semble valide, sauf si vous testez une méthode qui lève le même type d'exception dans plusieurs parties du code, à la seule différence que l'ID d'erreur, qui est transmis dans le message. Votre système peut afficher un message à l'utilisateur en fonction du message d'exception (et non du type d'exception). Dans ce cas, le message que l'utilisateur voit importe, vous devez donc tester le message d'erreur.
Vanja D.

34

Si vous utilisez PHP 5.5+, vous pouvez utiliser la ::classrésolution pour obtenir le nom de la classe avec expectException/setExpectedException . Cela offre plusieurs avantages:

  • Le nom sera pleinement qualifié avec son espace de noms (le cas échéant).
  • Il se résout en un stringdonc il fonctionnera avec n'importe quelle version de PHPUnit.
  • Vous obtenez la complétion de code dans votre IDE.
  • Le compilateur PHP émettra une erreur si vous saisissez mal le nom de la classe.

Exemple:

namespace \My\Cool\Package;

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

PHP compile

WrongPasswordException::class

dans

"\My\Cool\Package\WrongPasswordException"

sans PHPUnit étant le plus sage.

Remarque : PHPUnit 5.2 a été introduit expectException en remplacement de setExpectedException.


32

Le code ci-dessous testera le message d'exception et le code d'exception.

Important: il échouera si l'exception attendue n'est pas levée également.

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

6
$this->fail()n'est pas censé être utilisé de cette façon, je ne pense pas, du moins pas actuellement (PHPUnit 3.6.11); elle fait elle-même exception. En utilisant votre exemple, si $this->fail("Expected exception not thrown")est appelé, le catchbloc est déclenché et $e->getMessage()est "Exception attendue non levée" .
ken

1
@ken vous avez probablement raison. L'appel à failappartient probablement après le bloc catch, pas à l'intérieur de l'essai.
Frank Farmer

1
Je dois voter contre parce que l'appel à failne devrait pas être dans le trybloc. Il déclenche en soi le catchbloc produisant de faux résultats.
Twifty

6
Je crois que la raison pour laquelle cela ne fonctionne pas bien est qu'une situation est due à la capture de toutes les exceptions catch(Exception $e). Cette méthode fonctionne très bien pour moi lorsque j'essaie d'attraper des exceptions spécifiques:try { throw new MySpecificException; $this->fail('MySpecificException not thrown'); } catch(MySpecificException $e){}
spyle

23

Vous pouvez utiliser l' extension assertException pour affirmer plusieurs exceptions au cours d'une exécution de test.

Insérez la méthode dans votre TestCase et utilisez:

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

J'ai aussi fait un trait pour les amateurs de joli code ..


Quelle PHPUnit utilisez-vous? J'utilise PHPUnit 4.7.5, et il assertExceptionn'est pas défini. Je ne le trouve pas non plus dans le manuel PHPUnit.
physique du

2
La asertExceptionméthode ne fait pas partie de PHPUnit d'origine. Vous devez hériter la PHPUnit_Framework_TestCaseclasse et ajouter manuellement la méthode liée dans le post ci-dessus . Vos cas de test hériteront alors de cette classe héritée.
hejdav

18

Une autre manière peut être la suivante:

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

Veuillez vous assurer que votre classe de test s'étend \PHPUnit_Framework_TestCase.


Pour sûr, le plus de sucre dans cette syntaxe
AndrewMcLagan

13

PHPUnit expectException méthode est très gênante car elle permet de tester une seule exception par méthode de test.

J'ai créé cette fonction d'aide pour affirmer que certaines fonctions lèvent une exception:

/**
 * 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');
}

Ajoutez-le à votre classe de test et appelez ainsi:

public function testSomething() {
    $this->assertException(\PDOException::class, function() {
        new \PDO('bad:param');
    });
    $this->assertException(\PDOException::class, function() {
        new \PDO('foo:bar');
    });
}

Certainement la meilleure solution parmi toutes les réponses! Jetez-le dans un trait et emballez-le!
domdambrogia

11

Solution complète

Les " meilleures pratiques " actuelles de PHPUnit pour les tests d'exception semblent .. terne ( docs ).

Comme je voulais plus que l' expectExceptionimplémentation actuelle , j'ai fait un trait à utiliser sur mes cas de test. Ce ne sont que ~ 50 lignes de code .

  • Prend en charge plusieurs exceptions par test
  • Prend en charge les assertions appelées après la levée de l'exception
  • Exemples d'utilisation robustes et clairs
  • assertSyntaxe standard
  • Prend en charge les assertions pour plus qu'un simple message, code et classe
  • Prend en charge l'assertion inverse, assertNotThrows
  • Prend en charge les Throwableerreurs PHP 7

Bibliothèque

J'ai publié le AssertThrowstrait sur Github et packagist pour qu'il puisse être installé avec composer.

Exemple simple

Juste pour illustrer l'esprit derrière la syntaxe:

<?php

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

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

Génial?


Exemple d'utilisation complète

Veuillez voir ci-dessous pour un exemple d'utilisation plus complet:

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

?>

4
Un peu ironique que votre package pour les tests unitaires n'inclue pas de tests unitaires dans le référentiel.
domdambrogia

2
@domdambrogia grâce à @ jean-beguin il a désormais des tests unitaires.
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");
    }

}

La signature de assertEquals()est assertEquals(mixed $expected, mixed $actual...), inversée comme dans votre exemple, il devrait donc être$this->assertEquals("Exception message", $ex->getMessage());
Roger Campanera

7

Voici toutes les affirmations d'exception que vous pouvez faire. Notez que tous sont facultatifs .

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

La documentation peut être trouvée ici .


C'est incorrect car PHP s'arrête à la première exception levée. PHPUnit vérifie que l'exception levée a le bon type et dit «le test est OK», il ne connaît même pas la seconde exception.
Finesse

3
/**
 * @expectedException Exception
 * @expectedExceptionMessage Amount has to be bigger then 0!
 */
public function testDepositNegative()
{
    $this->account->deposit(-7);
}

Faites très attention "/**", notez le double "*". Écrire seulement "**" (astérisque) échouera votre code. Assurez-vous également que vous utilisez la dernière version de phpUnit. Dans certaines versions antérieures de phpunit, l'exception @expectedException n'est pas prise en charge. J'avais 4.0 et cela ne fonctionnait pas pour moi, j'ai dû mettre à jour vers 5.5 https://coderwall.com/p/mklvdw/install-phpunit-with-composer pour mettre à jour avec composer.


0

Pour PHPUnit 5.7.27 et PHP 5.6 et pour tester plusieurs exceptions dans un seul test, il était important de forcer le test d'exception. L'utilisation de la gestion des exceptions seule pour affirmer l'instance d'Exception sautera le test de la situation si aucune exception ne se produit.

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

}

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

voici le test

class FunctionTest extends \PHPUnit_Framework_TestCase{

   public function testException(){

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

   }

}

0

PhpUnit est une bibliothèque incroyable, mais ce point spécifique est un peu frustrant. C'est pourquoi nous pouvons utiliser la bibliothèque open source turbotesting-php qui a une méthode d'assertion très pratique pour nous aider à tester les exceptions. On le trouve ici:

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

Et pour l'utiliser, nous ferions simplement ce qui suit:

AssertUtils::throwsException(function(){

    // Some code that must throw an exception here

}, '/expected error message/');

Si le code que nous tapons à l'intérieur de la fonction anonyme ne lève pas d'exception, une exception sera levée.

Si le code que nous tapons à l'intérieur de la fonction anonyme lève une exception, mais que son message ne correspond pas à l'expression rationnelle attendue, une exception sera également levée.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.