Pruebas con PHPUnit¶
El curso pasado, dentro del módulo de Entornos de Desarrollo, se estudió la importancia de la realización de pruebas, así como las pruebas unitarias mediante JUnit.
A día de hoy es de gran importancia seguir una buena metodología de pruebas, siendo el desarrollo dirigido por las pruebas (Test Driven Development / TDD) uno de los enfoques más empleados, el cual consiste en:
- Escribir el test, y como no hay código implementado, la prueba falle (rojo).
- Escribir el código de aplicación para que la prueba funcione (verde).
- Refactorizar el código de la aplicación con la ayuda de la prueba para comprobar que no rompemos nada (refactor).
En el caso de PHP, la herramienta que se utiliza es PHPUnit (https://phpunit.de/), que como su nombre indica, está basada en JUnit. La versión actual es la 11.5.
Se recomienda consultar su documentación oficial.
Puesta en marcha¶
Las pruebas se almacenan en una carpeta tests
en el raíz del proyecto. Modificamos el archivo composer.json
:
"require-dev": {
"phpunit/phpunit": "^11.5"
},
"scripts": {
"test": "phpunit --testdox --colors tests"
}
También puedes instalar PHPUnit desde la terminal:
composer require --dev phpunit/phpunit ^11.5
Librerías de desarrollo
Las librerías que se colocan en require-dev
son las de desarrollo y testing, de manera que no se instalarán en un entorno de producción.
Como hemos creado un script, podemos lanzar las pruebas mediante:
composer test
Ejemplo de prueba:
<?php
use PHPUnit\Framework\TestCase;
class PilaTest extends TestCase
{
public function testPushAndPop()
{
$pila = [];
$this->assertSame(0, count($pila));
array_push($pila, 'batman');
$this->assertSame('batman', $pila[count($pila)-1]);
$this->assertSame(1, count($pila));
$this->assertSame('batman', array_pop($pila));
$this->assertSame(0, count($pila));
}
}
Métodos para ejecutar pruebas:
./vendor/bin/phpunit tests/PilaTest.php
./vendor/bin/phpunit tests
./vendor/bin/phpunit --testdox tests
./vendor/bin/phpunit --testdox --colors tests
Diseñando pruebas¶
Tal como hemos visto en el ejemplo, la clase de prueba debe heredar de TestCase
, y el nombre de la clase debe acabar en Test
, de ahí que hayamos llamado la clase de prueba como PilaTest
.
Una prueba implica un método de prueba (público) por cada funcionalidad a probar. Cada un de los métodos se les asocia un caso de prueba.
Los métodos deben nombrarse con el prefijo test
, por ejemplo, testPushAndPop
. Es muy importante que el nombre sea muy claro y descriptivo del propósito de la prueba. (camelCase).
En los casos de prueba prepararemos varias aserciones para toda la casuística: rangos de valores, tipos de datos, excepciones, etc...
Aserciones¶
Las aserciones permiten comprobar el resultado de los métodos que queremos probar. Las aserciones esperan que el predicado siempre sea verdadero.
PHPUnit ofrece las siguiente aserciones:
assertTrue
/assertFalse
: Comprueba que la condición dada sea evaluada como true / falseassertEquals
/assertSame
: Comprueba que dos variables sean igualesassertNotEquals
/assertNotSame
: Comprueba que dos variables NO sean igualesSame
→ comprueba los tipos. Si no coinciden los tipos y los valores, la aserción fallaráEquals
→ sin comprobación estricta
assertArrayHasKey
/assertArrayNotHasKey
: Comprueba que un array posea un key determinado / o NO lo poseaassertArraySubset
: Comprueba que un array posea otro array como subset del mismoassertAttributeContains
/assertAttributeNotContains
: Comprueba que un atributo de una clase contenga una variable determinada / o NO contenga una variable determinadaassertAttributeEquals
: Comprueba que un atributo de una clase sea igual a una variable determinada.
Comparando la salida¶
Si los métodos a probar generan contenido mediante echo
o una instrucción similar, disponemos de las siguiente expectativas:
expectOutputString(salidaEsperada)
expectOutputRegex(expresionRegularEsperada)
Las expectativas difieren de las aserciones que informan del resultado que se espera antes de invocar al método. Tras definir la expectativa, se invoca al método que realiza el echo
/print
.
<?php
namespace Dwes\Videoclub\Model;
use PHPUnit\Framework\TestCase;
use Dwes\Videoclub\Model\CintaVideo;
class CintaVideoTest extends TestCase {
public function testConstructor()
{
$cinta = new CintaVideo("Los cazafantasmas", 23, 3.5, 107);
$this->assertSame( $cinta->getNumero(), 23);
}
public function testMuestraResumen()
{
$cinta = new CintaVideo("Los cazafantasmas", 23, 3.5, 107);
$resultado = "<br>Película en VHS:";
$resultado .= "<br>Los cazafantasmas<br>3.5 (IVA no incluido)";
$resultado .= "<br>Duración: 107 minutos";
// definimos la expectativa
$this->expectOutputString($resultado);
// invocamos al método que hará echo
$cinta->muestraResumen();
}
}
Proveedores de datos¶
Cuando tenemos pruebas que solo cambian respecto a los datos de entrada y de salida, es útil utilizar proveedores de datos.
Se declaran en el docblock mediante @dataProvider nombreMetodo
, donde se indica el nombre de un método público que devuelve un array de arrays, donde cada elemento es un caso de prueba.
La clase de prueba recibe como parámetros los datos a probar y el resultado de la prueba como último parámetro.
El siguiente ejemplo comprueba con diferentes datos el funcionamiento de muestraResumen
:
<?php
/**
* @dataProvider cintasProvider
*/
public function testMuestraResumenConProvider($titulo, $id, $precio, $duracion, $esperado)
{
$cinta = new CintaVideo($titulo, $id, $precio, $duracion);
$this->expectOutputString($esperado);
$cinta->muestraResumen();
}
public function cintasProvider() {
return [
"cazafantasmas" => ["Los cazafantasmas", 23, 3.5, 107, "<br>Película en VHS:<br>Los cazafantasmas<br>3.5 €(IVA no incluido)<br>Duración: 107 minutos"],
"superman" => ["Superman", 24, 3, 188, "<br>Película en VHS:<br>Superman<br>3 € (IVA no incluido)<br>Duración: 188 minutos"],
];
}
Probando excepciones¶
Las pruebas además de comprobar que las clases funcionan como se espera, han de cubrir todos los casos posibles. Así pues, debemos poder hacer pruebas que esperen que se lance una excepción (y que el mensaje contenga cierta información):
Para ello, se utilizan las siguiente expectativas:
expectException(Excepcion::class)
expectExceptionCode(codigoExcepcion)
expectExceptionMessage(mensaje)
Del mismo modo que antes, primero se pone la expectativa, y luego se provoca que se lance la excepción:
<?php
public function testAlquilarCupoLleno() {
$soporte1 = new CintaVideo("Los cazafantasmas", 23, 3.5, 107);
$soporte2 = new Juego("The Last of Us Part II", 26, 49.99, "PS4", 1, 1);
$soporte3 = new Dvd("Origen", 24, 15, "es,en,fr", "16:9");
$soporte4 = new Dvd("El Imperio Contraataca", 4, 3, "es,en","16:9");
$cliente1 = new Cliente("Bruce Wayne", 23);
$cliente1->alquilar($soporte1);
$cliente1->alquilar($soporte2);
$cliente1->alquilar($soporte3);
$this->expectException(CupoSuperadoException::class);
$cliente1->alquilar($soporte4);
}
Cobertura de código¶
La cobertura de pruebas indica la cantidad de código que las pruebas cubren, siendo recomendable que cubran entre el 95 y el 100%.
Una de las métricas asociadas a los informes de cobertura es el CRAP (Análisis y Predicciones sobre el Riesgo en Cambios), el cual mide la cantidad de esfuerzo, dolor y tiempo requerido para mantener una porción de código. Esta métrica debe mantenerse con un valor inferior a 5.
Requisito xdebug
Aunque ya viene instalado dentro de PHPUnit, para que funcione la cobertura del código, es necesario que el código PHP se ejecute con XDEBUG, y e indicarle a Apache que así es (colocando en el archivo de configuración php.ini
la directiva xdebug.mode=coverage
).
Añadimos en composer.json
un nuevo script:
"coverage": "phpunit --coverage-html coverage --coverage-filter app tests"
Y posteriormente ejecutamos
composer coverage
Por ejemplo, si accedemos a la clase CintaVideo
con la prueba que habíamos realizado anteriormente, podemos observar la cobertura que tiene al 100% y que su CRAP es 2.

Temas pendientes
- Dependencia entre casos de prueba con el atributo
@depends
- Completamente configurable mediante el archivo
phpxml.xml
: https://phpunit.readthedocs.io/es/latest/configuration.html - Preparando las pruebas con
setUpBeforeClass()
ytearDownAfterClass()
- Objetos y pruebas Mock (dobles) con
createMock()
Actividades¶
AC 507. (RA4 RA5 / CE4f CE5h / IC1 / 3p) - A partir de la clase
HolaMonolog
, modifica los métodos para que además de escribir en en log, devuelvan el saludo como una cadena. Crea la claseHolaMonologTest
y añade diferentes casos de prueba para comprobar que los saludos y despedidas son acordes a la hora con la que se crea la clase.