Blog>

Roi Sánchez
01 Dic 2022

PhpUnit con tu tema de WordPress

Tiempo de lectura 9 minutos
  • brain monkey
  • buenas prácticas
  • clean code
  • mockery
  • php
  • phpunit
  • unit test
  • web
  • WordPress

Sumario Te gustaría hacer tests unitarios, pero piensas que como tu proyecto es un tema personalizado de WordPress no puedes hacerlo, ¡déjate de excusas! 😊

Empecemos viendo si realmente es posible hacer testing unitario con PhpUnit, con tu tema personalizado o plugin de WordPress.

La respuesta corta es sí.

La respuesta larga es sí, pero con algo de curro. 

Si quieres saber más sobre el testing y su relación con el clean code puedes leerte este magnífico artículo de mi compañero Carlos. 

El proceso para poder introducir pruebas unitarias en nuestro tema será: 

Preparar el código

Está claro que si quieres hacer test unitarios tu código tiene que estar preparado para ello. Un código espagueti, con mil dependencias, con clases y métodos gigantescos no es fácimente testeable. Si todavía programas así, antes de meterte a hacer testing unitario mírate nuestra lista de artículos de clean code, y no ess mala idea que te leas los libros de uncle Bob (Robert C. Martin). Si tu código es medianamente limpio puedes intentar meterle mano a los test unitarios. Ahora bien, en nuestro caso – PhpUnit con tu tema de WordPress, tenemos que hacer una segunda preparación del código. 

Para que realmente podamos testear nuestro código de manera unitaria y no integrada debemos poder probar ejecutar los tests sin necesidad de la funcionalidad de WordPress, y como mínimo vamos a necesitar poder cargar solo nuestro código y que las dependencias no necesiten de WordPress. La manera más normal de cargar nuestros ficheros de código en Wordpress sería usando una combinación de require_once y get_template_directory(). Algo así: 

require_once(get_template_directory() . '/models/seo_data.php'); 

Y solo con esto ya estamos fastidiados, tenemos dependencia de WordPress solo con usar un modelo de datos que hemos definido. 

Por ello el primer paso es configurar nuestro tema para usar autoload. 

Para poder usar autoload tenemos que seguir varios pasos. El primero es asegurarnos de que estamos siguiendo el estándar PSR-4. Básicamente lo que te dice este estándar es que los ficheros se deben llamar como las clases y los directorios como los namespaces, igual que harías en .net o Java. Además, todas nuestras clases deben estar bajo una carpeta concreta y a partir de ahí las carpetas según nuestro namespace. En nuestro caso, el tema se llama helloworld y hemos metido nuestras clases en otra carpeta inc. A partir de ahí tenemos nuestra estructura de namespace: 

Una vez que tienes esto hecho puedes meter este método en tu functions.php:

spl_autoload_register(function ($classname) { 
   $class      = str_replace('\\', DIRECTORY_SEPARATOR, str_replace('_', '-', strtolower($classname))); 
   $classes    = dirname(__FILE__) .  DIRECTORY_SEPARATOR . 'inc' . DIRECTORY_SEPARATOR . $class . '.php'; 
   if (file_exists($classes)) { 
      require_once($classes); 
   } 
}); 

Indicando en tu caso, en lugar de inc la carpeta en la que está tú código cargado. 

Nosotros para tener el código mejor organizado y cuando empezamos con las pruebas unitarias no tener mezclado código de test con código de nuestro ni con código de wordpress lo que hemos hecho es meter todo el site (WordPress con su wp-include, wp-content, etc) dentro de una carpeta src. De esta forma tendremos el site en la carpeta src, nuestros tests en la carpeta tests y la configuración general (de composer y demás) en la raíz. 

Lo siguiente es cambiar los require_once por use. Por ejemplo, para importar o usar una clase haríamos lo siguiente: 

use \helloworld\email_templates\SupportTemplate; 

Y en el cuerpo del método ya podríamos usar esta clase SupportTemplate: 

$support_template = new SupportTemplate($subject_support, $data); 

Por otro lado, si estábamos usando la técnica de incluir un fichero e incluir la llamada al constructor de la clase al acabar el fichero, ya no lo podemos hacer, porque no estamos haciendo un require y ejecutando el código, sino que solo estamos importando. Por tanto, desde donde lo importamos tendremos que llamar directamente al constructor. 

Probablemente según avances en tus tests tendrás que ir modificando tu código para mejorarlo y ser capaz de hacer tests unitarios sencillos y útiles. 

Instalar todo lo necesario

Composer

El primer paso es tener instalado composer para después instalar todo lo necesario. Composer es un gestor de dependencias para php, igual que tienes maven, nuget o npm para otros lenguajes. No nos vamos a meter en la explicación detallada de qué es composer ni qué pasos debes seguir para instalarlo, lo tienes todo aquí perfectamente explicado (en inglés, eso sí). 

Una vez que tienes composer instalado, asegúrate de que está correctamente instalado ejecutando  

Composer -V 

PhpUnit 

Una vez instalado composer, el siguiente paso es instalar PhpUnit. PhpUnit, como su propio autor dice, es un framework de testing de php orientado al programador. Al final es una instancia de la arquitectura xUnit para PHP, igual que nUnit es para .Net o jUnit para Java. 

Instalarlo es realmente sencillo. Las instrucciones las puedes seguir en la página de phpUnit, clickando en el enlace “Take the first steps”. De todas formas, indico aquí los pasos que he seguido yo. 

Lo primero es instalar phpunit. Para ello abro una consola (en mi caso la consola de VSCode) y me sitúo en la raíz del proyecto WordPress. Una vez situado ejecuto el siguiente comando: 

composer require --dev phpunit/phpunit ^9 

Y me aseguro de que se ha instalado correctamente ejecutando: 

./vendor/bin/phpunit --version 

Una vez hecho esto además de instalarme phpUnit ha debido crearme un fichero composer.json. Este fichero contendrá algo parecido a esto: 

{ 
  "require-dev": { 
    "phpunit/phpunit": "9" 
  } 
} 

Tendremos que añadirle una configuración para que phpUnit sepa a donde tiene que ir a buscar nuestras clases, dejando el fichero comoposer.json más o menos como este: 

{ 
  "autoload": { 
    "classmap": [ 
      "src/wp-content/themes/helloworld/inc/" 
    ] 
  }, 
  "require-dev": { 
    "phpunit/phpunit": "9" 
  } 
} 

Obviamente, la ruta exacta del classmap tendrás que cambiarla en función de tu código. 

Mockery 

Una de las necesidades comunes a la hora de programar nuestros tests es tener alguna herramienta de dobles de test. Para esto instalamos mockery con el siguiente comando:

composer require --dev mockery/mockery 

BrainMonkey 

El siguiente paquete a instalar es BrainMonkey que nos va a servir para hacer dobles de test de las funciones de WordPress, además ya tiene cierta funcionalidad implementada. 

El comando de instalación sería: 

composer require brain/monkey:2.* --dev 

Hacer tu primer test 

Una vez con todo instalado podemos proceder con nuestros tests 😊 

Bien, debemos tener una carpeta tests a nivel raíz del proyecto. Y dentro de esta carpeta iremos generando ficheros de test para cada uno de nuestros ficheros de código que vayamos a probar, siguiendo la misma estructura. Consiguiendo así que tanto nuestro código como nuestros tests estén perfectamente organizados y “sincronizados”. 

Ahora vamos a hacer un ejemplo de un test muy sencillo sin entrar a explicar todo lo que ofrece phpUnit ni su potencia, como decía vamos a quedarnos en un ejemplo de iniciación. A partir de ese punto con la documentación oficial tendrás que investigar todo lo que necesites para seguir. 

Vamos a probar un método de nuestro fichero ApiFaqs.php que se encuentra en la ruta src\wp-content\themes\helloworld\inc\helloworld\api\ApiFaqs.php. 

Para esto generamos el siguiente fichero de test tests\api\ApiFaqsTest.php. Como ves el nombre fichero del fichero tiene que acabar en Test para que phpUnit lo detecte. 

Aquí tienes el contenido básico de nuestra clase de test 

<?php 
declare(strict_types=1); 

use PHPUnit\Framework\TestCase; 

final class ApiFaqsTest extends TestCase 
{ 
    public function setUp(): void 
    { 
    } 
    public function tearDown(): void 
    { 
    } 
} 

Como puedes ver es una clase que extiende de TestCase y metemos dos métodos de inicialización y finalización de pruebas que son opcionales, y en nuestro ejemplo podríamos no haber usado. 

Nuestra clase debe extender de TestCase para que phpUnit sepa que es una clase de test y tenga la información necesaria para la ejecución. 

Ahora vamos a crear nuestro método de test. El nombre de este método debe comenzar por test. De esta forma phpUnit lo detectará automáticamente y vamos a utilizar esta convención para los nombres: 

test_[nombre método a probar]_Should_[lo que debe hacer]_When_[condición a probar] 

Internamente en nuestro método vamos a usar la estructura Given/When/Then. 

Veamos el ejemplo: 

public function test_add_post_faqs_module_to_cache_Should_AnhadirEndPoint_When_Noexiste() 
{ 
        //Given 
        $api = new \helloworld\api\ApiFaqs; 
        $allowed_endpoints = []; 

        //When 
        $allowed_endpoints = $api->add_post_faqs_module_to_cache($allowed_endpoints); 
        $returned = $allowed_endpoints[\helloworld\api\ApiConfig::SWEB_NAMESPACE][0]; 

        //Then 
        $this->assertEquals($returned, 'post/faqs_module'); 
} 

Como todo test acabamos con un assert que nos sirve para que el test sea autotesteable (Si no sabes cuales son los principios FIRST de los test te remito de nuevo a este artículo). 

Nuestros dobles de test 

Para generar nuestros dobles de test, ya sean de tipo stub o spy usamos mockery

Aquí puedes ver un ejemplo: 

public function test_get_faqs_module_Should_call_to_get_module_faqs_simple_page_When_template_is_page() 
{ 

  //Given 

  $ownMockSpy = \Mockery::mock('\helloworld\api\ApiFaqs')->makePartial(); 
  $postManagerMock = \Mockery::mock('alias:\helloworld\PostManager') 

    ->shouldReceive('get_post_called') 
    ->withAnyArgs() 

    ->andReturn(null) 

    ->getMock(); 

  $dataParam = \Mockery::mock('\WP_REST_Request') 

    ->shouldReceive('get_param') 

    ->with('template') 

    ->andReturn('page') 

    ->getMock(); 

  $api = new \helloworld\api\ApiFaqs; 

  //When 

  $response = $api->get_faqs_module($dataParam); 

  //Then 

  $ownMockSpy->shouldHaveReceived()->get_module_faqs_simple_page(); 

} 

En este caso hemos preparado 2 mocks, de forma que ajustamos los valores que van a devolver esas llamadas para no tener que probarlas en este test unitario y que se convierta en un test de integración. 

En el segundo caso si llamamos a un objeto de tipo WP_REST_Request solicitando el parámetro ‘template’ nos va a devolver page. 

$dataParam = \Mockery::mock('\WP_REST_Request') 
  ->shouldReceive('get_param') 
  ->with('template') 
  ->andReturn('page') 
  ->getMock(); 

Dobles de test de WordPress 

Lo siguiente es poder hacer mocks sobre las propias funciones de WordPress, ya que en ocasiones nuestro código varía en función de lo que no devuelva WordPress y por tanto eso es lo que debemos testear. En otras ocasiones simplemente necesitamos que nos devuelva algún dato de ejemplo para poder continuar con nuestro flujo. Para ello usamos la librearía BrainMonkey. Esta librería ya tiene una serie de funciones definidas, pero podemos ir nosotros definiendo lo que vayamos necesitando. 

Solo con tener use de Brain/Mokey a nivel de fichero y el use de MockeryPHPUnitIntegration a nivel de clase ya es capaz de hacer el mock de las funciones predefinidas. 

En nuestro ejemplo: 

use Brain\Monkey; 

final class ApiFaqsTest extends TestCase 
{ 
  use MockeryPHPUnitIntegration; 

Lo único importante es que no carguemos las clases antes de inicializar BrainMonkey. Por ejemplo, en nuestro caso estamos haciendo los add_action en el constructor. Por eso, para que BrainMonkey funcione tenemos que tener los require que necesitemos en nuestro método de test en lugar de en la cabecera del fichero. 

public function test_get_faqs_module_Should_call_to_get_module_faqs_simple_page_When_template_is_page() 
{ 
  require_once("src/wp-content/themes/helloworld/inc/helloworld/api/ApiConfig.php"); 

  require_once("src/wp-content/themes/helloworld/inc/helloworld/api/ApiFaqs.php"); 
  require_once("src/wp-content/themes/helloworld/inc/helloworld/api/ApiResponse.php"); 

Solo como nota, este es un punto que tenemos pendiente de mejorar en la configuración, ya que no debería ser necesario que incluyamos los require, con el autoload debería ser suficiente. 

También es posible mockear funciones de WordPress de las que no están predefinidas. 

Vamos a ver un caso real. Ahora para que los tests nos funcionen estamos haciendo una pequeña trampa. Hemos cambiado un método para que no tenga que acceder al método get_field que no tenemos falseado. 

private function get_module_faqs_simple_page($post) 
{ 
  $faqs_module = new stdClass; 

  return $faqs_module; 
  $faqs_module->show = get_field("mostrar_preguntas_frecuentes_simple_page", $post->ID); 
  if ($faqs_module->show) { 
    $mod_faqs_mod = get_field("preguntas_frecuentes_clone", $post->ID); 
    $faqs_module = $this->get_module_faqs_data($faqs_module, $mod_faqs_mod); 
  } 
  return $faqs_module; 
} 

Como se puede ver hemos metido un return antes del código en si. 

Lo que necesitamos simplemente es que get_field nos devuelva un false. 

Lo conseguimos de esta forma: 

Functions\when('get_field')->justReturn(false); 

Ejecutar tus tests

Este es el punto fácil. Una vez hecho todo lo anterior solo tenemos que ejecutar en nuestra consola el siguiente comando: 

./vendor/bin/phpunit tests 

Si además  tenemos configurado correctamente nuestro xDebug podremos depurar nuestros tests.  

Integración de los tests en CI 

Este punto lo dejamos para un futuro, ya que tiene entidad suficiente como para que tenga su propio artículo. 

Conclusión 

No temas en meter tests en cualquier proyecto, solo es un poco de sufrimiento al principio y en semanas mejorará tu calidad de sueño. 😊 

Referencias 

Autor

Roi Sánchez
Roi Sánchez

Desarrollador en dev&del

Capitán en Hello, World!

Capaz de gestionar un proyecto informático E2E (de principio a fin).

Los discos de vinilo y los tatuajes son dos de sus mayores pasiones.

¿Estás interesado?

Déjanos tus datos y contactaremos contigo lo antes posible