PHPUnit afirma que se lanzó una excepción?

337

¿Alguien sabe si hay assertalgo o algo así que puede probar si se produjo una excepción en el código que se está probando?

Felipe Almeida
fuente
2
A esas respuestas: ¿qué pasa con las aserciones múltiples en una función de prueba, y solo espero tener una excepción de lanzamiento? ¿Tengo que separarlos y poner el uno en una función de prueba independiente?
Panwen Wang

Respuestas:

550
<?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 () documentación de PHPUnit

El artículo del autor PHPUnit proporciona una explicación detallada sobre las mejores prácticas de excepciones de prueba.

Frank Farmer
fuente
8
Si usa espacios de nombres, debe ingresar el espacio de nombres completo:$this->setExpectedException('\My\Name\Space\MyCustomException');
Alcalyn
15
El hecho de que no pueda designar la línea precisa de código que se espera que arroje, es un error de la OMI. Y la imposibilidad de probar más de una excepción en la misma prueba hace que probar muchas excepciones esperadas sea un asunto realmente complicado. Escribí una afirmación real para tratar de resolver esos problemas.
mindplay.dk
18
FYI: a partir del método phpunit 5.2.0 setExpectedException está en desuso, reemplazado por el expectExceptionuno. :)
hejdav
41
Lo que no se menciona en los documentos o aquí, pero el código que se espera arroje una excepción debe llamarse después expectException() . Si bien podría haber sido obvio para algunos, fue una de gotcha para mí.
Jason McCreary
77
No es obvio del documento, pero no se ejecutará ningún código después de su función que arroje una excepción. Entonces, si desea probar varias excepciones en el mismo caso de prueba, no puede hacerlo.
Laurent
122

También puede usar una anotación docblock hasta que se lance PHPUnit 9:

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

Para PHP 5.5+ (especialmente con código de espacio de nombres), ahora prefiero usar ::class

David Harkness
fuente
3
OMI, este es el método preferido.
Mike Purcell
12
@LeviMorrison: en mi humilde opinión, el mensaje de excepción no debe probarse, de manera similar a los mensajes de registro. Ambos se consideran información extraña y útil cuando se realizan análisis forenses manuales . El punto clave a probar es el tipo de excepción. Cualquier cosa más allá de eso se vincula demasiado a la implementación. IncorrectPasswordExceptiondebería ser suficiente: que el mensaje sea igual "Wrong password for [email protected]"es auxiliar. Agregue a eso que desea pasar el menor tiempo posible escribiendo pruebas, y comenzará a ver cuán importantes se vuelven las pruebas simples.
David Harkness
55
@DavidHarkness, pensé que alguien mencionaría eso. Del mismo modo, estaría de acuerdo en que probar los mensajes en general es demasiado estricto y estricto. Sin embargo, es lo estricto y lo estricto que puede (enfatizado a propósito) lo que se desea en algunas situaciones, como la aplicación de una especificación.
Levi Morrison
1
No miraría en un doc-block para entender lo que esperaba, pero miraría el código de prueba real (independientemente del tipo de prueba). Ese es el estándar para todas las otras pruebas; No veo razones válidas para que las Excepciones sean (oh, Dios) una excepción a esta convención.
Kamafeather
3
La regla "no probar el mensaje" suena válida, a menos que pruebe un método que arroje el mismo tipo de excepción en varias partes del código, con la única diferencia que es la identificación del error, que se pasa en el mensaje. Su sistema puede mostrar un mensaje al usuario en función del mensaje de excepción (no del tipo de excepción). En ese caso, no importa qué mensaje vea el usuario, por lo tanto, debe probar el mensaje de error.
Vanja D.
34

Si está ejecutando PHP 5.5+, puede usar la ::classresolución para obtener el nombre de la clase con expectException/setExpectedException . Esto proporciona varios beneficios:

  • El nombre estará completamente calificado con su espacio de nombres (si corresponde).
  • Se resuelve en un stringmodo que funcionará con cualquier versión de PHPUnit.
  • Obtiene la finalización del código en su IDE.
  • El compilador de PHP emitirá un error si escribe mal el nombre de la clase.

Ejemplo:

namespace \My\Cool\Package;

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

PHP compila

WrongPasswordException::class

dentro

"\My\Cool\Package\WrongPasswordException"

sin PHPUnit siendo el más sabio.

Nota : PHPUnit 5.2 se introdujo expectException como un reemplazo para setExpectedException.

David Harkness
fuente
32

El siguiente código probará el mensaje de excepción y el código de excepción.

Importante: fallará si la excepción esperada no se produce también.

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
fuente
66
$this->fail()no está destinado a ser utilizado de esta manera, no creo, al menos no actualmente (PHPUnit 3.6.11); Actúa como una excepción en sí misma. Usando su ejemplo, si $this->fail("Expected exception not thrown")se llama, entonces el catchbloque se dispara y $e->getMessage()es "Excepción esperada no lanzada" .
Ken
1
@ken probablemente tengas razón. La llamada a failprobablemente pertenece después del bloque catch, no dentro del intento.
Frank Farmer
1
Tengo que votar a favor porque la llamada a failno debe estar en el trybloque. En sí mismo, activa el catchbloqueo produciendo resultados falsos.
Twifty
66
Creo que la razón por la que esto no funciona bien es que, en alguna situación, está atrapando todas las excepciones catch(Exception $e). Este método funciona bastante bien para mí cuando trato de capturar excepciones específicas:try { throw new MySpecificException; $this->fail('MySpecificException not thrown'); } catch(MySpecificException $e){}
spyle
23

Puede usar la extensión ClaimException para afirmar más de una excepción durante la ejecución de una prueba.

Inserte el método en su TestCase y use:

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

También hice un rasgo para los amantes del buen código ...

hejdav
fuente
¿Qué PHPUnit estás usando? Estoy usando PHPUnit 4.7.5, y no assertExceptionestá definido. Tampoco puedo encontrarlo en el manual de PHPUnit.
physicalattraction
2
El asertExceptionmétodo no es parte de PHPUnit original. Debe heredar la PHPUnit_Framework_TestCaseclase y agregar el método vinculado en la publicación anterior manualmente. Sus casos de prueba heredarán esta clase heredada.
hejdav
18

Una forma alternativa puede ser la siguiente:

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

Asegúrese de que su clase de prueba se extienda \PHPUnit_Framework_TestCase.

Antonis Charalambous
fuente
Sin duda, la mayor cantidad de azúcar en esta sintaxis
AndrewMcLagan
13

El expectExceptionmétodo PHPUnit es muy inconveniente porque permite probar solo una excepción por método de prueba.

He hecho esta función auxiliar para afirmar que alguna función arroja una excepción:

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

Agréguelo a su clase de prueba y llame de esta manera:

public function testSomething() {
    $this->assertException(\PDOException::class, function() {
        new \PDO('bad:param');
    });
    $this->assertException(\PDOException::class, function() {
        new \PDO('foo:bar');
    });
}
Finura
fuente
¡Definitivamente la mejor solución de todas las respuestas! ¡Tíralo en un rasgo y empaquétalo!
domdambrogia
11

Solución integral

Las " mejores prácticas " actuales de PHPUnit para pruebas de excepción parecen ... mediocres ( docs ).

Como quería más que la expectExceptionimplementación actual , hice un rasgo para usar en mis casos de prueba. Son solo ~ 50 líneas de código .

  • Admite múltiples excepciones por prueba
  • Admite aserciones llamadas después de que se lanza la excepción
  • Ejemplos de uso robustos y claros
  • assertSintaxis estándar
  • Admite aserciones para más que solo mensaje, código y clase
  • Apoya aserción inversa, assertNotThrows
  • Admite Throwableerrores PHP 7

Biblioteca

Publiqué el AssertThrowsrasgo en Github y Packagist para que pueda instalarse con el compositor.

Ejemplo simple

Solo para ilustrar el espíritu detrás de la sintaxis:

<?php

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

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

¿Con buena pinta?


Ejemplo de uso completo

Consulte a continuación un ejemplo de uso más completo:

<?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
fuente
44
Es un poco irónico que su paquete para pruebas unitarias no incluya pruebas unitarias en el repositorio.
domdambrogia
2
@domdambrogia gracias a @ jean-beguin ahora tiene pruebas unitarias.
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
fuente
La firma de assertEquals()es assertEquals(mixed $expected, mixed $actual...)inversa, como en su ejemplo, por lo que debería ser$this->assertEquals("Exception message", $ex->getMessage());
Roger Campanera
7

Aquí están todas las afirmaciones de excepción que puede hacer. Tenga en cuenta que todos ellos son opcionales .

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 documentación se puede encontrar aquí .

Westy92
fuente
Es incorrecto porque PHP se detiene en la primera excepción lanzada. PHPUnit comprueba que la excepción lanzada tiene el tipo correcto y dice «la prueba está bien», ni siquiera sabe acerca de la segunda excepción.
Finesse
3
/**
 * @expectedException Exception
 * @expectedExceptionMessage Amount has to be bigger then 0!
 */
public function testDepositNegative()
{
    $this->account->deposit(-7);
}

Tenga mucho cuidado "/**", observe el doble "*". Escribir solo "**" (asterisco) fallará su código. También asegúrese de estar usando la última versión de phpUnit. En algunas versiones anteriores de phpunit @expectedException Exception no es compatible. Tenía 4.0 y no me funcionó, tuve que actualizar a 5.5 https://coderwall.com/p/mklvdw/install-phpunit-with-composer para actualizar con el compositor.

C Cislariu
fuente
0

Para PHPUnit 5.7.27 y PHP 5.6 y para probar varias excepciones en una prueba, era importante forzar la prueba de excepción. El uso de manejo de excepciones solo para afirmar la instancia de Excepción omitirá probar la situación si no ocurre una excepción.

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
fuente
0
function yourfunction($a,$z){
   if($a<$z){ throw new <YOUR_EXCEPTION>; }
}

aqui esta la prueba

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
fuente
0

PhpUnit es una biblioteca increíble, pero este punto específico es un poco frustrante. Es por eso que podemos usar la biblioteca de código abierto turbotesting-php que tiene un método de afirmación muy conveniente para ayudarnos a probar excepciones. Se encuentra aquí:

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

Y para usarlo, simplemente haríamos lo siguiente:

AssertUtils::throwsException(function(){

    // Some code that must throw an exception here

}, '/expected error message/');

Si el código que escribimos dentro de la función anónima no produce una excepción, se generará una excepción.

Si el código que escribimos dentro de la función anónima produce una excepción, pero su mensaje no coincide con la expresión regular esperada, también se generará una excepción.

Jaume Mussons Abad
fuente