PHPUnit

PHPUnit es un representante de los marcos de pruebas unitarias, cuya idea general es proporcionar un contrato estricto sobre un fragmento de código aislado que debe cumplirse. Este fragmento de código es lo que llamamos unidad, que se traduce en la clase y sus métodos en PHP. Usando la funcionalidad de aserciones, el marco PHPUnit verifica que estas unidades se comporten como se esperaba. El beneficio de las pruebas unitarias es que su detección temprana de problemas ayuda a mitigar los errores compuestos o en la línea que podrían no ser evidentes inicialmente. Cuantas más rutas posibles de un programa cubra la prueba unitaria, mejor.

Configurar PHPUnit

PHPUnit se puede instalar como, provisionalmente nombrado, una herramienta o una biblioteca. En realidad, ambas son las mismas cosas, solo que difieren en la forma en que las instalamos y las usamos. La versión de la herramienta es realmente solo un archivo phar de PHP que podemos ejecutar a través de la consola, que luego proporciona un conjunto de comandos de consola que podemos ejecutar globalmente. La versión de la biblioteca, por otro lado, es un conjunto de
La versión de la biblioteca, por otro lado, es un conjunto de bibliotecas PHPUnit empaquetadas como un paquete Composer, así como un binario que se descarga en el directorio vendor/bin/ del proyecto.
Suponiendo que estamos usando la instalación de Ubuntu 16.10 (Yakkety Yak), instalar PHPUnit como herramienta es fácil mediante los siguientes comandos:

wget https://phar.phpunit.de/phpunit.phar
chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit
phpunit --version

Esto debería darnos el resultado final, al igual que la siguiente captura de pantalla:

PHPUnit se convierte en una herramienta de consola accesible en todo el sistema, no relacionada con ningún proyecto específicamente.
Instalar PHPUnit como biblioteca es tan fácil como ejecutar el siguiente comando de consola dentro de la raíz de nuestro proyecto:

composer require phpunit/phpunit

Esto debería darnos el resultado final, al igual que la siguiente captura de pantalla:

Esto instala todos los archivos de la biblioteca PHPUnit dentro del directorio vendor/phpunit/ de nuestro proyecto, así como el archivo ejecutable phpunit en el directorio vendor/bin/.

Configurar una aplicación de muestra

Antes de comenzar a escribir algunos scripts de prueba de PHPUnit, sigamos adelante y creemos una aplicación muy simple que consta de solo unos pocos archivos. Esto nos permitirá enfocarnos en la esencia de escribir una prueba más adelante.

El desarrollo impulsado por pruebas (TDD), como el realizado con PHPUnit, anima a escribir pruebas antes de las implementaciones. De esta manera, las pruebas establecen las expectativas para la funcionalidad y no al revés.
Este enfoque requiere un cierto nivel de experiencia y disciplina, que podría no ser adecuado para los recién llegados a PHPUnit.

Supongamos que estamos formando parte de la funcionalidad de compra en la web, por lo tanto, para empezar, tratamos con entidades de productos y categorías. La primera clase que abordamos es el modelo del producto.
Lo haremos creando el archivo src\Foggyline\Catalog\Model\Product.php, con su contenido de la siguiente manera:

<?php
declare(strict_types=1);
namespace Foggyline\Catalog\Model;
class Product
{
protected $id;
protected $title;
protected $price;
protected $taxRate;
public function __construct(string $id, string $title, float $price,
int $taxRate)
{
$this->id = $id;
$this->title = $title;
$this->price = $price;
$this->taxRate = $taxRate;
}
public function getId(): string
{
return $this->id;
}
public function getTitle(): string
{
return $this->title;
}
public function getPrice(): float
{
return $this->price;
}
public function getTaxRate(): int
{
return $this->taxRate;
}
}

La clase Product se basa en el constructor para configurar la ID, el título, el precio y la tasa impositiva del producto. Aparte de eso, no existe una lógica real para la clase, aparte de los métodos getter simples. Con la clase de producto en su lugar, sigamos adelante y creemos una clase de categoría. Nosotros
lo agregará al archivo src\Foggyline\Catalog\Model\Category.php, con su contenido de la siguiente manera:

<?php
declare(strict_types=1);
namespace Foggyline\Catalog\Model;
class Category
{
protected $title;
protected $products;
public function __construct(string $title, array $products)
{
$this->title = $title;
$this->products = $products;
}
public function getTitle(): string
{
return $this->title;
}
public function getProducts(): array
{
return $this->products;
}
}

La clase Category se basa en el constructor para configurar el título de la categoría y sus productos. Aparte de eso, no hay lógica, aparte de los dos métodos getter, que simplemente devuelven los valores establecidos a través del constructor.

Para animar un poco las cosas, con fines de prueba, sigamos adelante y creemos una clase de capa ficticia como parte del archivo src\Foggyline\Catalog\Model\Layer.php, con su contenido como sigue:

<?php
namespace Foggyline\Catalog\Model;
// Just a dummy class, for testing purpose
class Layer
{
public function dummy()
{
$time = time();
sleep(2);
$time = time() - $time;
return $time;
}
}

Usaremos esta clase simplemente como un ejemplo, con el análisis de cobertura de código más adelante.
Con los modelos de Producto y Categoría, sigamos adelante y creemos la clase
Block\Category\View como parte del archivo src\Foggyline\Catalog\Block\Category\View.php, con su contenido de la siguiente manera:

<?php
declare(strict_types=1);
namespace Foggyline\Catalog\Block\Category;
use Foggyline\Catalog\Model\Category;
class View
{
protected $category;
public function __construct(Category $category)
{
$this->category = $category;
}
public function render(): string
{
$products = ''; foreach ($this->category->getProducts() as $product) {
if ($product instanceof \Foggyline\Catalog\Model\Product) {
$products .= '<div class="product">
<h1 class="product-title">' . $product->getTitle() . '</h1> <div class="product-price">' .
number_format($product->getPrice(), 2, ',', '.') . '</h1>
</div>';
}
}
return '<div class="category">
<h1 class="category-title">' . $this->category->getTitle() . '</h1>
<div class="category-products">' . $products . '</div> </div>';
}
}

Estamos utilizando el método render() para representar la página de categoría completa. La página en sí consiste en un título de categoría y un contenedor de todos sus productos con sus títulos y precios individuales. Ahora que tenemos nuestras clases de aplicaciones verdaderamente básicas descritas, agreguemos un simple cargador de tipo PSR4 al archivo autoload.php, con su contenido de la siguiente manera:

<?php
$loader = require __DIR__ . '/vendor/autoload.php';
$loader->addPsr4('Foggyline\\'
, __DIR__ . '/src/Foggyline');

Finalmente, configuramos el punto de entrada a nuestra aplicación como parte del archivo index.php, con su contenido de la siguiente manera:

<?php
require __DIR__ . '/autoload.php';
use Foggyline\Catalog\Model\Product;
use Foggyline\Catalog\Model\Category;
use Foggyline\Catalog\Block\Category\View as CategoryView;
$category = new Category('Laptops', [

new Product('RL', 'Red Laptop', 1499.99, 25),

new Product('YL', 'Yellow Laptop', 2499.99, 25),

new Product('BL', 'Blue Laptop', 3499.99, 25),
]);
$categoryView = new CategoryView($category);
echo $categoryView->render();

Utilizaremos esta aplicación completamente simple en otros tipos de pruebas también, por lo que vale la pena tener en cuenta sus archivos y estructura.

Examen de escritura

Comenzar a escribir pruebas de PHPUnit requiere comprender algunos conceptos básicos, como los siguientes:

  • El método setUp(): análogo al constructor, aquí es donde creamos los objetos contra los cuales realizaremos la prueba.
  • El método tearDown(): análogo al destructor, aquí es donde limpiamos los objetos contra los que realizamos la prueba.
  • Los métodos test *(): cada método público cuyo nombre comienza con test, por ejemplo, testSomething(), testItAgain(), etc., se considera una prueba única. Se puede lograr el mismo efecto agregando la anotación @test en el docblock de un método; aunque, este parece ser un caso menos usado.
  • La anotación @depends: esto permite expresar dependencias entre los métodos de prueba.
  • Afirmaciones: el corazón de PHPUnit, este conjunto de métodos nos permite razonar sobre la corrección.

El archivo vendor\phpunit\phpunit\src\Framework\Assert\Functions.php contiene una lista extensa de las declaraciones de la función assert*,
tales como assertEquals(), assertContains(), assertLessThan(), y otros, totalizando más de 90 diferentes funciones de assert.

Con esto en mente, sigamos adelante y escribamos el archivo src\Foggyline\Catalog\Test\Unit\Model\ProductTest.php, con su contenido de la siguiente manera:

<?php
namespace Foggyline\Catalog\Test\Unit\Model;
use PHPUnit\Framework\TestCase;
use Foggyline\Catalog\Model\Product;
class ProductTest extends TestCase
{

protected $product;

public function setUp()
{

$this->product = new Product('SL', 'Silver Laptop', 4599.99, 25);
}

public function testTitle()
{

$this->assertEquals(

'Silver Laptop',

$this->product->getTitle()
);
}

public function testPrice()
{

$this->assertEquals(

4599.99,

$this->product->getPrice()
);
}
}

Nuestra clase ProductTest está utilizando un método setUp() para configurar una instancia de una clase Product. Los dos métodos test*() luego usan el método incorporado de PHPUnit assertEquals() para probar el valor del título y el precio del producto.
Luego agregamos el archivo src\Foggyline\Catalog\Test\Unit\Model\CategoryTest.php,
con su contenido de la siguiente manera:

<?php
namespace Foggyline\Catalog\Test\Unit\Model;
use PHPUnit\Framework\TestCase;
use Foggyline\Catalog\Model\Product;
use Foggyline\Catalog\Model\Category;
class CategoryTest extends TestCase
{

protected $category;

public function setUp()
{

$this->category = new Category('Laptops', [

new Product('TRL', 'Test Red Laptop', 1499.99, 25),

new Product('TYL', 'Test Yellow Laptop', 2499.99, 25),
]);
}

public function testTotalProductsCount()
{

$this->assertCount(2, $this->category->getProducts());
}

public function testTitle()
{

$this->assertEquals('Laptops', $this->category->getTitle());
}
}

Nuestra clase CategoryTest está utilizando un método setUp() para configurar una instancia de una clase Category, junto con los dos productos pasados al constructor de la clase Category.
Los dos métodos test*() luego usan los métodos incorporado de PHPUnit assertCount() y
assertEquals() para probar los valores instanciados.
Luego agregamos el archivo src\Foggyline\Catalog\Test\Unit\Block\Category\ViewTest.php, con su contenido de la siguiente manera:

<?php
namespace Foggyline\Catalog\Test\Unit\Block\Category;
use PHPUnit\Framework\TestCase;
use Foggyline\Catalog\Model\Product;
use Foggyline\Catalog\Model\Category;
use Foggyline\Catalog\Block\Category\View as CategoryView;
class ViewTest extends TestCase
{

protected $category;

protected $categoryView;

public function setUp()
{

$this->category = new Category('Laptops', [

new Product('TRL', 'Test Red Laptop', 1499.99, 25),

new Product('TYL', 'Test Yellow Laptop', 2499.99, 25),
]);

$this->categoryView = new CategoryView($this->category);
}

public function testCategoryTitle()
{

$this->assertContains(
'<h1 class="category-title">Laptops',

$this->categoryView->render()
);
}

public function testProductsContainer()
{

$this->assertContains(

'<h1 class="product-title">Test Yellow',

$this->categoryView->render()
);
}
}

Nuestra clase ViewTest está utilizando un método setUp() para configurar una instancia de una clase Category, junto con los dos productos pasados al constructor de la clase Category. Los dos métodos de test*() luego utilizan el método incorporado de PHPUnit assertContains() para probar la presencia del valor que debe devolverse a través de la llamada al método render() de vista de categoría.
Luego agregamos el archivo phpunit.xml, con su contenido de la siguiente manera:

<phpunit bootstrap="autoload.php">
<testsuites>
<testsuite name="foggyline">
<directory>src/Foggyline/*/Test/Unit/*</directory>
</testsuite>
</testsuites>
</phpunit>

El archivo de configuración phpunit.xml admite una lista bastante sólida de opciones. Usando el atributo bootstrap de un elemento PHPUnit, estamos instruyendo a la herramienta PHPUnit para que cargue el archivo autoload.php antes de ejecutar las pruebas. Esto asegura que nuestro autocargador PSR4 se activará y que nuestras clases de prueba verán nuestras clases dentro del directorio src/Foggyline
directorio. El conjunto de pruebas de foggyline que definimos en testsuites utiliza la opción de directorio para especificar, en forma de expresiones regulares, la ruta a nuestras pruebas unitarias. La ruta que utilizamos fue tal que todos los archivos de los directorios src/Foggyline/Catalog/Test/Unit/ y posiblemente src/Foggyline/Checkout/Test/Unit/son recogidos.

Echa un vistazo a https://phpunit.de/manual/current/en/appendixes.configuration.html para más información sobre las opciones de configuración de phpunit.xml.

Ejecutando pruebas

Ejecutar el conjunto de pruebas que acabamos de escribir es tan fácil como ejecutar el comando phpunit dentro de nuestro directorio raíz del proyecto.
Tras la ejecución, phpunit buscará el archivo phpunit.xml y actuará en consecuencia. Esto significa que phpunit sabrá dónde buscar los archivos de prueba. Las pruebas ejecutadas con éxito muestran una salida como la siguiente captura de pantalla:

Sin embargo, las pruebas ejecutadas sin éxito muestran una salida como la siguiente captura de pantalla:

Podemos modificar fácilmente una de las clases de prueba, como lo hicimos con el ViewTest anterior, para activar y observar las reacciones de phpunit a las fallas.

Cobertura de código

Lo mejor de PHPUnit es su funcionalidad de informes de cobertura de código. Podemos agregar fácilmente cobertura de código a nuestro conjunto de pruebas simplemente extendiendo el archivo phpunit.xml de la siguiente manera:

<phpunit bootstrap="autoload.php">
<testsuites>
<testsuite name="foggyline">
<directory>src/Foggyline/*/Test/Unit/*</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>src/Foggyline/</directory>
<exclude>
<file>src/config.php</file>
<file>src/auth.php</file>
<directory>src/Foggyline/*/Test/</directory>
</exclude>
</whitelist>
<logging>
<log type="coverage-html" target="log/report" lowUpperBound="50"
highLowerBound="80"/>
</logging>
</filter>
</phpunit>

Aquí, agregamos el elemento de filtro, con una lista blanca adicional y un elemento de registro. Ahora podemos activar la prueba nuevamente, pero, esta vez, con un comando ligeramente modificado, de la siguiente manera:

phpunit --coverage-html log/report

Esto debería darnos el resultado final, como se muestra en la siguiente captura de pantalla:

El directorio log/report ahora debería estar lleno de archivos de informes HTML. Si lo exponemos al navegador, podemos ver un informe bien generado con valiosa información sobre nuestra base de código, como se muestra en la siguiente captura de pantalla:

La captura de pantalla anterior muestra un porcentaje de cobertura de código en
la estructura de directorios src/Foggyline/Catalog/. Profundizando más en un directorio Model, vemos que nuestra clase Layer tiene una cobertura de código del 0%, lo cual se espera, ya que no hemos escrito ninguna prueba para ello:

Profundizando más en la clase de Producto real, podemos ver la cobertura del código PHPUnit que describe todas y cada una de las líneas de código cubiertas por nuestra prueba:

Mirar directamente en la clase de capa real nos da una buena visión de la falta de cobertura de código dentro de esta clase:

La cobertura de código proporciona información valiosa visual y estadística sobre la cantidad de código que hemos cubierto con las pruebas. Aunque esta información se malinterpreta fácilmente, tener una cobertura de código del 100% no es una medida de nuestra calidad de prueba individual. Escribir pruebas de calidad requiere que el escritor, es decir, el desarrollador, comprenda claramente qué es exactamente la prueba unitaria. Dice que podemos tener fácilmente una cobertura de código del 100%, con pruebas de aprobación del 100% y, sin embargo, no podemos abordar ciertos casos de prueba o rutas de lógica.

Comparte