PHPUnit zapewnia, że ​​został zgłoszony wyjątek?

337

Czy ktoś wie, czy istnieje assertcoś takiego, co może przetestować, czy w testowanym kodzie został zgłoszony wyjątek?

Felipe Almeida
źródło
2
Na te odpowiedzi: co z wieloma asercjami w funkcji testowej, a ja po prostu oczekuję jednego wyjątku dotyczącego rzucania? Czy muszę je rozdzielić i wprowadzić niezależną funkcję testową?
Panwen Wang

Odpowiedzi:

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

expectException () Dokumentacja PHPUnit

Artykuł autora PHPUnit zawiera szczegółowe objaśnienia dotyczące testowania najlepszych praktyk dotyczących wyjątków.

Frank Farmer
źródło
8
Jeśli korzystasz z przestrzeni nazw, musisz wprowadzić pełną przestrzeń nazw:$this->setExpectedException('\My\Name\Space\MyCustomException');
Alcalyn
15
Fakt, że nie można wyznaczyć dokładnego wiersza kodu, który ma zostać wygenerowany, jest błędem IMO. Brak możliwości przetestowania więcej niż jednego wyjątku w tym samym teście sprawia, że ​​testowanie wielu oczekiwanych wyjątków jest naprawdę niezręczną sprawą. Napisałem rzeczywiste stwierdzenie, aby spróbować rozwiązać te problemy.
mindplay.dk
18
FYI: od phpunit 5.2.0 setExpectedException metoda jest przestarzała, zastąpiona tą expectException. :)
hejdav
41
Co nie jest wymienione w docs lub tutaj, ale kod oczekuje się rzucić potrzeb wyjątków być nazywany po expectException() . Choć dla niektórych mogło to być oczywiste, dla mnie była to gotcha .
Jason McCreary
7
Nie jest to oczywiste z dokumentu, ale żaden kod po twojej funkcji, który zgłasza wyjątek, nie zostanie wykonany. Więc jeśli chcesz przetestować wiele wyjątków w tym samym przypadku testowym, nie możesz.
laurent
122

Możesz także używać adnotacji docblock do czasu wydania PHPUnit 9:

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

W przypadku PHP 5.5+ (szczególnie z kodem przestrzeni nazw) wolę teraz używać ::class

David Harkness
źródło
3
IMO, jest to preferowana metoda.
Mike Purcell,
12
@LeviMorrison - IMHO komunikat wyjątku nie powinien być testowany, podobnie jak komunikaty dziennika. Oba są uważane za obce, pomocne informacje podczas ręcznej analizy sądowej. Kluczowym punktem do przetestowania jest rodzaj wyjątku. Wszystko poza tym wiąże się zbyt ściśle z implementacją. IncorrectPasswordExceptionpowinno wystarczyć - aby komunikat "Wrong password for [email protected]"był pomocniczy. Dodaj do tego, że chcesz poświęcić jak najmniej czasu na pisanie testów, a zaczniesz widzieć, jak ważne stają się proste testy.
David Harkness
5
@DavidHarkness Pomyślałem, że ktoś to podniesie. Podobnie zgodziłbym się, że testowanie wiadomości ogólnie jest zbyt surowe i ścisłe. Jednak to właśnie ścisłość i ścisłe wiązanie może (celowo podkreślić) być tym, czego pragnie się w niektórych sytuacjach, takich jak egzekwowanie specyfikacji.
Levi Morrison,
1
Nie oglądałbym w bloku dokumentów, żeby zrozumieć, czego się spodziewał, ale patrzyłem na rzeczywisty kod testowy (niezależnie od rodzaju testu). To jest standard dla wszystkich innych testów; Nie widzę ważnych powodów, dla których Wyjątki były (o Boże) wyjątkiem od tej konwencji.
Kamafeather
3
Reguła „nie testuj komunikatu” brzmi poprawnie, chyba że przetestujesz metodę, która generuje ten sam typ wyjątku w wielu częściach kodu, a jedyną różnicą jest identyfikator błędu przekazywany w komunikacie. Twój system może wyświetlać użytkownikowi komunikat na podstawie komunikatu wyjątku (nie typu wyjątku). W takim przypadku nie ma znaczenia, który komunikat widzi użytkownik, dlatego należy przetestować komunikat o błędzie.
Vanja D.,
34

Jeśli korzystasz z PHP 5.5+, możesz użyć ::classrozdzielczości, aby uzyskać nazwę klasy za pomocą expectException/setExpectedException . Zapewnia to kilka korzyści:

  • Nazwa będzie w pełni kwalifikowana z jej przestrzenią nazw (jeśli istnieje).
  • Rozwiązuje się stringtak, więc będzie działać z każdą wersją PHPUnit.
  • Otrzymasz uzupełnianie kodu w swoim IDE.
  • Kompilator PHP wygeneruje błąd, jeśli pomylisz nazwę klasy.

Przykład:

namespace \My\Cool\Package;

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

Kompiluje PHP

WrongPasswordException::class

w

"\My\Cool\Package\WrongPasswordException"

bez PHPUnit jest mądrzejszy.

Uwaga : PHPUnit 5.2 wprowadzony expectException jako zamiennik setExpectedException.

David Harkness
źródło
32

Poniższy kod przetestuje komunikat o wyjątku i kod wyjątku.

Ważne: zakończy się niepowodzeniem, jeśli nie zostanie zgłoszony oczekiwany wyjątek.

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
źródło
6
$this->fail()nie powinien być używany w ten sposób, nie sądzę, przynajmniej obecnie (PHPUnit 3.6.11); sam stanowi wyjątek. Korzystając z Twojego przykładu, jeśli $this->fail("Expected exception not thrown")zostanie wywołany, catchblok zostanie wyzwolony i $e->getMessage()zostanie wyświetlony komunikat „Oczekiwany wyjątek nie został zgłoszony” .
Ken
1
@ken prawdopodobnie masz rację. Wywołanie do failprawdopodobnie należy po bloku catch, a nie wewnątrz try.
Frank Farmer
1
Muszę przegłosować, ponieważ wezwanie do failnie powinno być w trybloku. To samo w sobie wyzwala catchblok, który daje fałszywe wyniki.
Twifty
6
Uważam, że powodem tego nie jest dobra sytuacja, ponieważ łapie wszystkie wyjątki catch(Exception $e). Ta metoda działa dla mnie całkiem dobrze, gdy próbuję wychwycić określone wyjątki:try { throw new MySpecificException; $this->fail('MySpecificException not thrown'); } catch(MySpecificException $e){}
spyle
23

Rozszerzenia assertException można użyć do potwierdzenia więcej niż jednego wyjątku podczas jednego wykonania testu.

Wstaw metodę do TestCase i użyj:

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

Stworzyłem również cechę dla miłośników dobrego kodu ..

hejdav
źródło
Z którego PHPUnit korzystasz? Korzystam z PHPUnit 4.7.5 i nie assertExceptionjest zdefiniowany. Nie mogę go również znaleźć w podręczniku PHPUnit.
fizyczna
2
asertExceptionMetoda nie jest częścią oryginalnego PHPUnit. Musisz odziedziczyć PHPUnit_Framework_TestCaseklasę i ręcznie dodać metodę podaną w poście powyżej . Twoje przypadki testowe odziedziczą tę odziedziczoną klasę.
hejdav
18

Alternatywnym sposobem może być:

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

Upewnij się, że twoja klasa testowa sięga \PHPUnit_Framework_TestCase.

Antonis Charalambous
źródło
Na pewno najwięcej cukru w ​​tej składni
AndrewMcLagan
13

Metoda PHPUnit expectExceptionjest bardzo niewygodna, ponieważ pozwala przetestować tylko jeden wyjątek na metodę testową.

Zrobiłem tę funkcję pomocnika, aby zapewnić, że jakaś funkcja zgłasza wyjątek:

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

Dodaj go do klasy testowej i wywołaj w ten sposób:

public function testSomething() {
    $this->assertException(\PDOException::class, function() {
        new \PDO('bad:param');
    });
    $this->assertException(\PDOException::class, function() {
        new \PDO('foo:bar');
    });
}
Impas
źródło
Zdecydowanie najlepsze rozwiązanie spośród wszystkich odpowiedzi! Rzuć to w cechę i spakuj!
domdambrogia
11

Kompleksowe rozwiązanie

Obecne „ najlepsze praktyki ” PHPUnit dotyczące testowania wyjątków wydają się… brakiem połysku ( dokumenty ).

Ponieważ chciałem więcej niż obecna expectExceptionimplementacja, postanowiłem wykorzystać tę cechę w moich testach. To tylko ~ 50 linii kodu .

  • Obsługuje wiele wyjątków na test
  • Obsługuje asercje wywoływane po zgłoszeniu wyjątku
  • Solidne i jasne przykłady użycia
  • Standard assert składnia
  • Obsługuje asercje dotyczące nie tylko wiadomości, kodu i klasy
  • Obsługuje odwrotne twierdzenie, assertNotThrows
  • Obsługuje Throwablebłędy PHP 7

Biblioteka

Opublikowałem AssertThrowscechę Github i packagist, aby można ją było zainstalować z kompozytorem.

Prosty przykład

Aby zilustrować ducha składni:

<?php

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

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

Dość schludny?


Pełny przykład użycia

Poniżej znajduje się pełniejszy przykład użycia:

<?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
źródło
4
Trochę ironiczne, że twój pakiet do testów jednostkowych nie zawiera testów jednostkowych w repozytorium.
domdambrogia
2
@domdambrogia dzięki @ jean-beguin ma teraz testy jednostkowe.
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
źródło
Podpis assertEquals()jest assertEquals(mixed $expected, mixed $actual...)odwrotny jak w twoim przykładzie, więc powinien być$this->assertEquals("Exception message", $ex->getMessage());
Roger Campanera,
7

Oto wszystkie możliwe wyjątki. Pamiętaj, że wszystkie są opcjonalne .

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

Dokumentacja znajduje się tutaj .

Westy92
źródło
Jest to nieprawidłowe, ponieważ PHP zatrzymuje się przy pierwszym zgłoszonym wyjątku. PHPUnit sprawdza, czy zgłoszony wyjątek ma poprawny typ i mówi „test jest OK”, nawet nie wie o drugim wyjątku.
Finezja
3
/**
 * @expectedException Exception
 * @expectedExceptionMessage Amount has to be bigger then 0!
 */
public function testDepositNegative()
{
    $this->account->deposit(-7);
}

Bądź bardzo ostrożny "/**", zauważ podwójne „*”. Pisanie tylko „**” (asterix) zawiedzie w twoim kodzie. Upewnij się także, że używasz ostatniej wersji phpUnit. W niektórych wcześniejszych wersjach phpunit @expectedException Wyjątek nie jest obsługiwany. Miałem 4.0 i nie działało to dla mnie, musiałem zaktualizować do wersji 5.5 https://coderwall.com/p/mklvdw/install-phpunit-with-composer, aby zaktualizować za pomocą kompozytora.

C Cislariu
źródło
0

W przypadku PHPUnit 5.7.27 i PHP 5.6 oraz w celu przetestowania wielu wyjątków w jednym teście ważne było wymuszenie testowania wyjątków. Użycie samej obsługi wyjątków w celu potwierdzenia wystąpienia wyjątku spowoduje pominięcie testowania sytuacji, jeśli nie wystąpi wyjątek.

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

}
kwas
źródło
0
function yourfunction($a,$z){
   if($a<$z){ throw new <YOUR_EXCEPTION>; }
}

tutaj jest test

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
źródło
0

PhpUnit to niesamowita biblioteka, ale ten konkretny punkt jest nieco frustrujący. Dlatego możemy użyć biblioteki opensource turbotesting-php, która ma bardzo wygodną metodę asercji, która pomaga nam testować wyjątki. Można go znaleźć tutaj:

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

Aby go użyć, po prostu wykonalibyśmy następujące czynności:

AssertUtils::throwsException(function(){

    // Some code that must throw an exception here

}, '/expected error message/');

Jeśli kod, który wpisujemy w funkcji anonimowej, nie zgłasza wyjątku, wyjątek zostanie zgłoszony.

Jeśli kod, który wpisujemy w funkcji anonimowej, zgłasza wyjątek, ale jego komunikat nie pasuje do oczekiwanego wyrażenia regularnego, wyjątek również zostanie zgłoszony.

Jaume Mussons Abad
źródło