Hay estudios que demuestran que casi todos los fallos críticos en software es el mal manejo de errores. La idea es encontrar estos huecos lo más rápido posible, ya sea haciendo testing automático o exploratorio para conseguir que nuestro software sea más robusto.
Hacer test para estos casos normalmente es complicado, hay casos de prueba que cuesta mucho reproducir y otros muchos que ni siquiera tenemos en mente que puedan producirse. Pero esto no debe desanimarnos a la hora de hacer un tratamiento de excepciones correcto.
Las excepciones proporcionan ventajas: el fallo es rápido, no hay que pensar en el valor de retorno de la función, no interrumpe la ejecución del programa y en algunos casos podemos mejorar la interconexión con sistemas externos. Para ellos tenemos que evitar que haya más log de la cuenta y escribir mensajes de log que nos ayuden saber que iba a pasar después (antes de que se saltase la excepción), aunqie bien es cierto que no debemos jugar con las excepciones (eso de… lanzo una excepción aquí y capturo allí para luego… no hace fácil seguir con el flujo de ejecución)
Excepciones en PHP
Con PHP es sencillo utilizar excepciones y muchos frameworks las utilizan e incluso hacen que todos los errores se muestren como excepciones. Por ejemplo Laravel muestra todos los errores como excepciones utilizando Whoops! siempre que este habilitado el modo debug (app.debug).
No obstante PHP es bastante laxo en el uso de excepciones, por ejemplo podemos llamar file_get_contents()
y nos devolverá un FALSE
.
¿Qué hacemos?
Vamos a asumir que el método al que llamamos lanza una excepción conocida. ¿Qué hacemos? Existen un montón de posibilidades y aquí vamos a mostrar algunas de ellas.
Capturar e ignorar
try{
methodCall()
} catch (Exception $e){}
En general esta es una mala solución porque estamos perdiendo toda la información. Sin embargo hay casos donde este patrón podría ser valido. Por ejemplo en un bloque finally
para asegurarnos que no estamos machacando una excepción anterior (generalmente más importante) podemos llamar a este caso: ignored
Capturar y log
try{
methodCall()
} catch (Exception $e){
$this->logger->warning('Failed to do FOO with BAR');
}
Aquí seguimos ignorando pero dejamos registro de que algo pasó. Este patrón solo debemos utilizarlo en algunas ocasiones, ya que pasa lo mismo que con el anterior: el flujo del programa continua y podría ser que más adelante apareciera un error debido a que aquí no se hizo nada
try{
$foo = methodCall()
} catch (Exception $e){
$this->logger->warning('Failed to do FOO with BAR');
}
if(!empty($foo)){
...
}
El problema aquí es que estamos incluyendo un if en el programa, una mejor situación podría ser
Capturar, log y manejar
try{
$fechedContent = fetch($url)
} catch (Exception $e){
$this->logger->warning('Failed to fetch ' . $url. 'Will use a empty String');
$fechedContent = '';
}
Aquí el manejo de la excepción está implícito en el bloque catch. Intentaremos utilizar siempre valores neutros que no influyan en la ejecución del programa. Una variante podría ser un “early return”
try{
$fechedContent = fetch($url)
} catch (Exception $e){
$this->logger->warning('Failed to fetch ' . $url. 'Will use a empty String');
return null;
Capturar y lanzar una nueva excepción
try{
$fechedContent = fetch($url)
} catch (Exception $e){
throw new RuntimeExcepcion('Failed to fetch ' . $url);
Así enmascaramos posibles errores al interactuar con otros sistemas y podemos construir cadenas de excepciones que nos ayudarán a saber que es lo que sucedió.
No capturar
La excepción no se captura y listo. Esto es similar a capturar y “lanzar una nueva excepción” con la diferencia de que no añadimos contexto ni información a la excepción lanzada.
¿Qué variante usamos?
Depende mucho de lo que estemos haciendo pero lo mejor es siempre mantener el control y flujo en nuestro programa, por ello “capturar, log y manejar” es una opción a tener siempre en cuenta.
Esto no significa que siempre tengamos que utilizarla, ya que en muchos casos podría ser conveniente “capturar y lanzar una nueva excepción” o incluso “no capturar”.
<?php
$email = new Fuel\Email;
$email->subject('My Subject');
$email->body('How the heck are you?');
$email->to('guy@example.com', 'Some Guy');
try
{
$email->send();
}
catch(Fuel\Email\ValidationFailedException $e)
{
// La validación falló
}
catch(Fuel\Email\SendingFailedException $e)
{
// El envío no ha podido realizarse
}
finally
{
// Ejecutado independientemente de que se lanzase una excepción
}
¿Cómo testar excepciones?
Para testar excepciones podemos usar la notación @expectedException
<?php
class ExampleTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException ExpectedException
*/
public function testExpectedExceptionIsRaised ( )
{
// Arrange
$example = new Example ;
// Act
$example -> doSomething ( ) ;
}
}
En este ejemplo, la anotación @expectedException
le dice a PHPUnit que el test de debajo lanzará una excepción de un tipo determinado. Si la excepción se lanza el test será considerado como correcto, sino el test será marcado como incorrecto.
Además podemos añadir las anotaciones @expectedExceptionCode
, @expectedExceptionMessage
y @expectedExceptionMessageRegExp
Ahora desde PHPUnit 5.2 podemos utilizar expectedException
para así evitar el engorro de hacer anotaciones
<?php
namespace vendor \ project ;
class ExampleTest extends \ PHPUnit_Framework_TestCase
{
public function testExpectedExceptionIsRaised ( )
{
// Arrange
$example = new Example ;
// Assert
$this -> expectException ( ExpectedException :: class ) ;
// Act
$example -> foo ( ) ;
}
}
De esta manera tenemos el código en 3 fases, lo que hace que sea más fácil de leer. De la misma manera también existen los métodos expectExceptionCode()
, expectExceptionMessage
y expectExceptionMessageRegExp()
Conclusiones
Con este post a tenemos un mayor conocimiento acerca de como tratar las excepciones. De la misma manera aprendemos a perderles el miedo y utilizarlas para hacer que nuestro software sea más robusto.
Un comentario en “Usando excepciones para escribir mejor software”