Probando callbacks de hooks
Estoy desarrollando un plugin usando TDD y una cosa que no logro probar correctamente son... los hooks.
Quiero decir, está bien, puedo probar el callback de un hook, pero ¿cómo podría probar si un hook realmente se dispara (tanto hooks personalizados como los hooks por defecto de WordPress)? Supongo que algún tipo de mocking ayudaría, pero simplemente no logro entender qué me falta.
Instalé el conjunto de pruebas con WP-CLI. Según esta respuesta, el hook init
debería dispararse, pero... no lo hace; aunque el código funciona correctamente dentro de WordPress.
Según mi entendimiento, el bootstrap se carga al final, así que tiene sentido que no dispare init, así que la pregunta que queda es: ¿cómo diablos debería probar si los hooks se están disparando?
¡Gracias!
El archivo bootstrap se ve así:
$_tests_dir = getenv('WP_TESTS_DIR');
if ( !$_tests_dir ) $_tests_dir = '/tmp/wordpress-tests-lib';
require_once $_tests_dir . '/includes/functions.php';
function _manually_load_plugin() {
require dirname( __FILE__ ) . '/../includes/RegisterCustomPostType.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
require $_tests_dir . '/includes/bootstrap.php';
El archivo que estoy probando se ve así:
class RegisterCustomPostType {
function __construct()
{
add_action( 'init', array( $this, 'register_post_type' ) );
}
public function register_post_type()
{
register_post_type( 'foo' );
}
}
Y la prueba en sí:
class CustomPostTypes extends WP_UnitTestCase {
function test_custom_post_type_creation()
{
$this->assertTrue( post_type_exists( 'foo' ) );
}
}
¡Gracias!

Prueba en aislamiento
Al desarrollar un plugin, la mejor manera de probarlo es sin cargar el entorno de WordPress.
Si escribes código que pueda probarse fácilmente sin WordPress, tu código se vuelve mejor.
Cada componente que se prueba unitariamente debe probarse en aislamiento: cuando pruebas una clase, solo debes probar esa clase específica, asumiendo que todo el resto del código funciona perfectamente.
Esta es la razón por la que las pruebas unitarias se llaman "unitarias".
Como beneficio adicional, sin cargar el núcleo, tus pruebas se ejecutarán mucho más rápido.
Evita hooks en el constructor
Un consejo que puedo darte es evitar poner hooks en los constructores. Eso es una de las cosas que harán que tu código sea probado en aislamiento.
Veamos el código de prueba en OP:
class CustomPostTypes extends WP_UnitTestCase {
function test_custom_post_type_creation() {
$this->assertTrue( post_type_exists( 'foo' ) );
}
}
Y asumamos que esta prueba falla. ¿Quién es el culpable?
- ¿el hook no se agregó en absoluto o no se agregó correctamente?
- ¿el método que registra el tipo de publicación no se llamó en absoluto o con argumentos incorrectos?
- ¿hay un error en WordPress?
¿Cómo se puede mejorar?
Asumamos que el código de tu clase es:
class RegisterCustomPostType {
function init() {
add_action( 'init', array( $this, 'register_post_type' ) );
}
public function register_post_type() {
register_post_type( 'foo' );
}
}
(Nota: me referiré a esta versión de la clase por el resto de la respuesta)
La forma en que escribí esta clase te permite crear instancias de la clase sin llamar a add_action
.
En la clase anterior hay 2 cosas que probar:
- el método
init
realmente llama aadd_action
pasándole los argumentos correctos - el método
register_post_type
realmente llama a la funciónregister_post_type
No dije que tengas que verificar si el tipo de publicación existe: si agregas la acción correcta y si llamas a register_post_type
, el tipo de publicación personalizado debe existir: si no existe, es un problema de WordPress.
Recuerda: cuando pruebas tu plugin, debes probar tu código, no el código de WordPress. En tus pruebas debes asumir que WordPress (como cualquier otra biblioteca externa que uses) funciona bien. Ese es el significado de prueba unitaria.
Pero... ¿en la práctica?
Si WordPress no está cargado, si intentas llamar a los métodos de la clase anterior, obtienes un error fatal, por lo que necesitas simular las funciones.
El método "manual"
Claro que puedes escribir tu propia biblioteca de simulación o simular "manualmente" cada método. Es posible. Te mostraré cómo hacerlo, pero luego te mostraré un método más fácil.
Si WordPress no está cargado mientras se ejecutan las pruebas, significa que puedes redefinir sus funciones, como add_action
o register_post_type
.
Asumamos que tienes un archivo, cargado desde tu archivo bootstrap, donde tienes:
function add_action() {
global $counter;
if ( ! isset($counter['add_action']) ) {
$counter['add_action'] = array();
}
$counter['add_action'][] = func_get_args();
}
function register_post_type() {
global $counter;
if ( ! isset($counter['register_post_type']) ) {
$counter['register_post_type'] = array();
}
$counter['register_post_type'][] = func_get_args();
}
Reescribí las funciones para simplemente agregar un elemento a un array global cada vez que se llaman.
Ahora deberías crear (si no tienes uno ya) tu propia clase base de caso de prueba que extienda PHPUnit_Framework_TestCase
: eso te permite configurar fácilmente tus pruebas.
Puede ser algo como:
class Custom_TestCase extends \PHPUnit_Framework_TestCase {
public function setUp() {
$GLOBALS['counter'] = array();
}
}
De esta manera, antes de cada prueba, el contador global se restablece.
Y ahora tu código de prueba (me refiero a la clase reescrita que publiqué arriba):
class CustomPostTypes extends Custom_TestCase {
function test_init() {
global $counter;
$r = new RegisterCustomPostType;
$r->init();
$this->assertSame(
$counter['add_action'][0],
array( 'init', array( $r, 'register_post_type' ) )
);
}
function test_register_post_type() {
global $counter;
$r = new RegisterCustomPostType;
$r->register_post_type();
$this->assertSame( $counter['register_post_type'][0], array( 'foo' ) );
}
}
Deberías notar:
- Pude llamar a los dos métodos por separado y WordPress no está cargado en absoluto. De esta manera, si una prueba falla, sé exactamente quién es el culpable.
- Como dije, aquí pruebo que las clases llaman a las funciones de WP con los argumentos esperados. No hay necesidad de probar si el CPT realmente existe. Si estás probando la existencia del CPT, entonces estás probando el comportamiento de WordPress, no el comportamiento de tu plugin...
Bonito... ¡pero es un dolor de cabeza!
Sí, si tienes que simular manualmente todas las funciones de WordPress, es realmente doloroso. Algunos consejos generales que puedo darte es usar la menor cantidad posible de funciones de WP: no tienes que reescribir WordPress, sino abstraer las funciones de WP que usas en clases personalizadas, para que puedan simularse y probarse fácilmente.
Por ejemplo, respecto al ejemplo anterior, puedes escribir una clase que registre tipos de publicación, llamando a register_post_type
en 'init' con los argumentos dados.
Con esta abstracción, aún necesitas probar esa clase, pero en otros lugares de tu código que registran tipos de publicación puedes hacer uso de esa clase, simulándola en pruebas (asumiendo que funciona).
Lo increíble es que si escribes una clase que abstrae el registro de CPT, puedes crear un repositorio separado para ella, y gracias a herramientas modernas como Composer incrustarla en todos los proyectos donde la necesites: prueba una vez, usa en todas partes. Y si alguna vez encuentras un error en ella, puedes corregirlo en un solo lugar y con un simple composer update
todos los proyectos donde se usa se corrigen también.
Por segunda vez: escribir código que sea probado en aislamiento significa escribir mejor código.
Pero tarde o temprano necesitaré usar funciones de WP en algún lugar...
Por supuesto. Nunca debes actuar en paralelo al núcleo, no tiene sentido. Puedes escribir clases que envuelvan funciones de WP, pero esas clases también deben probarse. El método "manual" descrito anteriormente puede usarse para tareas muy simples, pero cuando una clase contiene muchas funciones de WP puede ser un dolor.
Afortunadamente, hay buenas personas que escriben cosas buenas. 10up, una de las mayores agencias de WP, mantiene una biblioteca muy buena para las personas que quieren probar plugins de la manera correcta. Es WP_Mock
.
Te permite simular funciones y hooks de WP. Asumiendo que lo has cargado en tus pruebas (lee el readme del repositorio) la misma prueba que escribí anteriormente se convierte en:
class CustomPostTypes extends Custom_TestCase {
function test_init() {
$r = new RegisterCustomPostType;
// prueba que la acción se agregó con los argumentos dados
\WP_Mock::expectActionAdded( 'init', array( $r, 'register_post_type' ) );
$r->init();
}
function test_register_post_type() {
// prueba que la función se llamó con los argumentos dados y se ejecutó una vez
\WP_Mock::wpFunction( 'register_post_type', array(
'times' => 1,
'args' => array( 'foo' ),
) );
$r = new RegisterCustomPostType;
$r->register_post_type();
}
}
Simple, ¿no? Esta respuesta no es un tutorial para WP_Mock
, así que lee el readme del repositorio para más información, pero el ejemplo anterior debería ser bastante claro, creo.
Además, no necesitas escribir ninguna función simulada add_action
o register_post_type
por ti mismo, o mantener ninguna variable global.
¿Y las clases de WP?
WP tiene algunas clases también, y si WordPress no está cargado cuando ejecutas pruebas, necesitas simularlas.
Eso es mucho más fácil que simular funciones, PHPUnit tiene un sistema integrado para simular objetos, pero aquí quiero sugerirte Mockery. Es una biblioteca muy poderosa y muy fácil de usar. Además, es una dependencia de WP_Mock
, así que si lo tienes, tienes Mockery también.
Pero ¿qué pasa con WP_UnitTestCase
?
El conjunto de pruebas de WordPress fue creado para probar el núcleo de WordPress, y si quieres contribuir al núcleo es fundamental, pero usarlo para plugins solo hace que pruebes no en aislamiento.
Mira más allá del mundo WP: hay muchos marcos y CMS modernos de PHP por ahí y ninguno sugiere probar plugins/módulos/extensiones (o como se llamen) usando código del marco.
Si extrañas las fábricas, una característica útil del conjunto, debes saber que hay cosas increíbles por ahí.
Trampas y desventajas
Hay un caso en el que el flujo de trabajo que sugerí aquí falla: pruebas de base de datos personalizadas.
De hecho, si usas tablas estándar de WordPress y funciones para escribir allí (en el nivel más bajo métodos de $wpdb
) nunca necesitarás realmente escribir datos o probar si los datos están realmente en la base de datos, solo asegúrate de que se llamen los métodos adecuados con los argumentos adecuados.
Sin embargo, puedes escribir plugins con tablas personalizadas y funciones que construyan consultas para escribir allí, y probar si esas consultas funcionan es tu responsabilidad.
En esos casos, el conjunto de pruebas de WordPress puede ayudarte mucho, y cargar WordPress puede ser necesario en algunos casos para ejecutar funciones como dbDelta
.
(No hace falta decir que uses una base de datos diferente para las pruebas, ¿verdad?)
Afortunadamente, PHPUnit te permite organizar tus pruebas en "suites" que pueden ejecutarse por separado, por lo que puedes escribir un suite para pruebas de bases de datos personalizadas donde cargues el entorno de WordPress (o parte de él) dejando el resto de tus pruebas libres de WordPress.
Solo asegúrate de escribir clases que abstraigan tantas operaciones de base de datos como sea posible, de manera que todas las demás clases del plugin las utilicen, para que, usando simulaciones, puedas probar adecuadamente la mayoría de las clases sin lidiar con la base de datos.
Por tercera vez, escribir código que pueda probarse fácilmente en aislamiento significa escribir mejor código.

¡Santo cielo, mucha información útil! ¡Gracias! De alguna manera, logré pasar por alto todo el punto de las pruebas unitarias (hasta ahora, solo practicaba pruebas de PHP dentro de Code Dojo). También descubrí wp_mock hoy mismo, pero por alguna razón logré ignorarlo.
Lo que me molestaba es que cualquier prueba, sin importar lo pequeña que fuera, solía tomar al menos dos segundos en ejecutarse (primero cargar el entorno de WP, luego ejecutar la prueba).
¡Gracias de nuevo por abrirme los ojos!

Gracias @IonutStaicu, olvidé mencionar que no cargar WordPress hace que tus pruebas sean mucho más rápidas

En efecto. Porque en Dojo tenía un tiempo de ejecución de menos de 1-200ms (pocas pruebas, 20-30 como máximo); ¿el conjunto de pruebas de WP? suficiente para sacarme del flujo.

También vale la pena señalar que el marco de pruebas unitarias de WP Core es una herramienta increíble para ejecutar pruebas de INTEGRACIÓN, que serían pruebas automatizadas para asegurar que se integra bien con el propio WP (por ejemplo, que no haya colisiones accidentales de nombres de funciones, etc.).

@JohnPBloch +1 por el buen punto. Aunque usar un espacio de nombres sea suficiente para evitar colisiones de nombres de funciones en WordPress, donde todo es global :) Pero, claro, las pruebas de integración/funcionales son importantes. Actualmente estoy jugando con Behat + Mink pero todavía estoy practicando con eso.

Gracias por el "paseo en helicóptero" sobre el bosque de UnitTest de WordPress - todavía me estoy riendo de esa imagen épica ;-)

@gmazzap Esta respuesta parece cubrir muchos de los problemas que me estaban dando dolores de cabeza. Sin embargo, o me estoy perdiendo un punto, o no cubre el caso donde necesito probar una clase que crea una instancia de otra clase (por ejemplo, WP_Post): Pensé que factory-muffin podría cubrir esto, pero esto funciona cuando la instancia de la clase se pasa como dependencia. En este caso, la instancia de la clase es creada dentro de la clase que estoy probando.

@andrea-sciamanna el problema es que un código "testeable" bien escrito debería evitar crear instancias de objetos dentro de otros objetos (a menos que sean objetos de PHP como ArrayObject
u objetos de valor), por esa misma razón. Si eso sucede, y no puedes o no quieres cambiar el código, la única manera de hacer pruebas "unitarias" de esos objetos es crear "stubs". Por ejemplo, puedes escribir una clase personalizada WP_Post
, que solo se cargue en las pruebas. Eso no es fácil de mantener, y es por eso que programar con pruebas en mente, o incluso escribir pruebas antes del código (TDD) es algo bueno :)

Creo que esto puede merecer una pregunta separada, especialmente porque no hay suficiente espacio para seguir escribiendo aquí :) Pregunta: http://wordpress.stackexchange.com/questions/233132/unit-tests-dealing-with-dependencies
