Prueba de devolución de llamada de ganchos

34

Estoy desarrollando un complemento con TDD y una cosa que no puedo probar son ... ganchos.

Quiero decir, OK, puedo probar la devolución de llamada de gancho, pero ¿cómo podría probar si un gancho realmente se dispara (ganchos personalizados y ganchos predeterminados de WordPress)? Supongo que algunas burlas ayudarán, pero simplemente no puedo entender lo que me estoy perdiendo.

Instalé el conjunto de pruebas con WP-CLI. Según esta respuesta , el initgancho debería activarse, pero ... no lo hace; Además, el código funciona dentro de WordPress.

Según tengo entendido, el bootstrap se carga al final, por lo que tiene sentido no activar init, por lo que la pregunta que queda es: ¿cómo diablos debería probar si se activan los ganchos?

¡Gracias!

El archivo de arranque 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 probado 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!

Ionut Staicu
fuente
Si está ejecutando phpunit, ¿puede ver pruebas fallidas o pasadas? ¿Instalaste bin/install-wp-tests.sh?
Sven
Creo que parte del problema es que quizás RegisterCustomPostType::__construct()nunca se llama cuando se carga el complemento para las pruebas. También es posible que esté siendo afectado por el error # 29827 ; quizás intente actualizar su versión del conjunto de pruebas unitarias de WP.
JD
@Sven: sí, las pruebas están fallando; He instalado bin/install-wp-tests.sh(que no uso wp-CLI) @JD: RegisterCustomPostType :: __ construct se llama (acaba de añadir una die()declaración y PHPUnit se detiene allí)
Ionut Staicu
No estoy muy seguro del lado de las pruebas unitarias (no es mi punto fuerte), pero desde el punto de vista literal puede usar did_action()para verificar si las acciones se dispararon.
Rarst
@Rarst: gracias por la sugerencia, pero aún no funciona. Por alguna razón, creo que el momento es incorrecto (las pruebas se ejecutan antes del initenlace).
Ionut Staicu

Respuestas:

72

Prueba aislada

Al desarrollar un complemento, la mejor manera de probarlo es sin cargar el entorno de WordPress.

Si escribe código que se puede probar fácilmente sin WordPress, su código se vuelve mejor .

Todos los componentes que se prueban por unidad deben probarse de forma aislada : cuando prueba una clase, solo tiene que probar esa clase específica, suponiendo que el resto del código esté funcionando perfectamente.

El aislador

Esta es la razón por la cual las pruebas unitarias se denominan "unidad".

Como beneficio adicional, sin cargar el núcleo, su prueba se ejecutará mucho más rápido.

Evitar ganchos en constructor

Un consejo que puedo darle es evitar poner ganchos en los constructores. Esa es una de las cosas que hará que su código sea comprobable de forma aislada.

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 supongamos que esta prueba falla . ¿Quién es el culpable ?

  • ¿No se agregó el gancho o no fue correcto?
  • ¿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?

Supongamos que su código de 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 para el resto de la respuesta)

La forma en que escribí esta clase le permite crear instancias de la clase sin llamar add_action.

En la clase anterior hay 2 cosas para ser probadas:

  • el método init realmente llama add_actionpasarle argumentos apropiados
  • el método register_post_type realmente llama a la register_post_typefunción

No dije que debe verificar si el tipo de publicación existe: si agrega la acción adecuada y si llama register_post_type, debe existir el tipo de publicación personalizada : si no existe, es un problema de WordPress.

Recuerde: cuando prueba su complemento, debe probar su código, no el código de WordPress. En sus pruebas, debe asumir que WordPress (al igual que cualquier otra biblioteca externa que use) funciona bien. Ese es el significado de la prueba unitaria .

Pero ... en la práctica?

Si WordPress no está cargado, si intenta llamar a los métodos de clase anteriores, obtendrá un error fatal, por lo que debe burlarse de las funciones.

El método "manual"

Claro que puede escribir su biblioteca de burla o burlarse "manualmente" de cada método. Es posible. Te diré cómo hacerlo, pero luego te mostraré un método más fácil.

Si WordPress no se carga mientras se ejecutan las pruebas, significa que puede redefinir sus funciones, por ejemplo, add_actiono register_post_type.

Supongamos que tiene un archivo, cargado desde su archivo bootstrap, donde tiene:

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 una matriz global cada vez que se llaman.

Ahora debe crear (si aún no tiene uno) su propia clase de caso de prueba base que se extienda PHPUnit_Framework_TestCase: eso le permite configurar fácilmente sus 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 reinicia.

Y ahora su código de prueba (me refiero a la clase reescrita que publiqué anteriormente):

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' ) );
  }

}

Debes tener en cuenta:

  • 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 CPT realmente existe. Si está probando la existencia de CPT, está probando el comportamiento de WordPress, no el comportamiento de su complemento ...

Bonito ... pero es una PITA!

Sí, si tiene que burlarse manualmente de todas las funciones de WordPress, es realmente un dolor. Algunos consejos generales que puedo dar es usar la menor cantidad posible de funciones de WP: no es necesario reescribir WordPress, sino funciones abstractas de WP que se usan en clases personalizadas, de modo que se puedan burlar y probar fácilmente.

Por ejemplo, con respecto al ejemplo anterior, puede escribir una clase que registre tipos de publicaciones, invocando register_post_type'init' con argumentos dados. Con esta abstracción, aún necesita probar esa clase, pero en otros lugares de su código que registran tipos de publicaciones, puede hacer uso de esa clase, burlándose de ella en las pruebas (suponiendo que funcione).

Lo increíble es que, si escribe una clase que resuma el registro de CPT, puede crear un repositorio separado para él, y gracias a las herramientas modernas como Composer, incrustarlo en todos los proyectos donde lo necesite: probar una vez, usar en todas partes . Y si alguna vez encuentra un error en él, puede solucionarlo en un solo lugar y con un simple composer updatetodos los proyectos donde se utiliza también se solucionan.

Por segunda vez: escribir código que se pueda probar de forma aislada significa escribir un código mejor.

Pero tarde o temprano necesito usar las funciones de WP en alguna parte ...

Por supuesto. Nunca debes actuar en paralelo al núcleo, no tiene sentido. Puede escribir clases que envuelvan las 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 una molestia.

Afortunadamente, allí hay buenas personas que escriben cosas buenas. 10up , una de las mayores agencias de WP, mantiene una gran biblioteca para las personas que desean probar los complementos de la manera correcta. Es WP_Mock.

Le permite burlarse de las funciones de WP y los ganchos . Suponiendo que haya cargado en sus pruebas (vea el archivo readme del repositorio), la misma prueba que escribí anteriormente se convierte en:

class CustomPostTypes extends Custom_TestCase {

  function test_init() {
     $r = new RegisterCustomPostType;
     // tests that the action was added with given arguments
     \WP_Mock::expectActionAdded( 'init', array( $r, 'register_post_type' ) );
     $r->init();
  }

  function test_register_post_type() {
     // tests that the function was called with given arguments and run once
     \WP_Mock::wpFunction( 'register_post_type', array(
        'times' => 1,
        'args' => array( 'foo' ),
     ) );
     $r = new RegisterCustomPostType;
     $r->register_post_type();
  }

}

Simple, ¿no es así? Esta respuesta no es un tutorial para WP_Mock, así que lea el archivo readme del repositorio para obtener más información, pero el ejemplo anterior debería ser bastante claro, creo.

Además, no necesita escribir ninguna burla add_actiono register_post_typeusted mismo, ni mantener ninguna variable global.

¿Y las clases de WP?

WP también tiene algunas clases, y si WordPress no se carga cuando ejecuta pruebas, debe burlarse de ellas.

Eso es mucho más fácil que burlarse de funciones, PHPUnit tiene un sistema integrado para burlarse de objetos, pero aquí quiero sugerirle burla . Es una biblioteca muy poderosa y muy fácil de usar. Además, es una dependencia de WP_Mock, por lo que si lo tiene, también tiene burla.

¿Pero que pasa WP_UnitTestCase?

El conjunto de pruebas de WordPress se creó para probar el núcleo de WordPress , y si desea contribuir al núcleo es fundamental, pero usarlo para complementos solo hace que la prueba no sea aislada.

Eche un vistazo al mundo de WP: hay muchos frameworks PHP modernos y CMS por ahí y ninguno de ellos sugiere probar plugins / módulos / extensiones (o como se llamen) usando el código de marco.

Si echa de menos las fábricas, una característica útil de la suite, debe saber que hay cosas increíbles allí.

Gotchas y desventajas

Hay un caso en el que falta el flujo de trabajo que sugerí aquí: pruebas de bases de datos personalizadas .

De hecho, si utiliza tablas de WordPress y funciones estándar para escribir allí (al nivel más bajo $wpdbmétodos) que no será necesario en realidad datos de escritura o de prueba si los datos es en realidad la base de datos, sólo asegúrese de que los métodos adecuados se llaman con argumentos adecuados.

Sin embargo, puede escribir complementos con tablas y funciones personalizadas que crean consultas para escribir allí, y probar si esas consultas funcionan es su responsabilidad.

En esos casos, el conjunto de pruebas de WordPress puede ayudarlo mucho, y puede ser necesario cargar WordPress en algunos casos para ejecutar funciones como dbDelta.

(No hay necesidad de decir que use una base de datos diferente para las pruebas, ¿no es así?)

Afortunadamente, PHPUnit le permite organizar sus pruebas en "suites" que se pueden ejecutar por separado, por lo que puede escribir una suite para pruebas de bases de datos personalizadas donde cargue el entorno de WordPress (o parte de él) dejando todo el resto de sus pruebas sin WordPress .

Solo asegúrese de escribir clases que resuman tantas operaciones de base de datos como sea posible, de manera que todas las demás clases de complementos las utilicen, de modo que utilizando simulacros pueda probar adecuadamente la mayoría de las clases sin tener que lidiar con la base de datos.

Por tercera vez, escribir código fácilmente comprobable de forma aislada significa escribir un código mejor.

gmazzap
fuente
55
Santa mierda, mucha información útil! ¡Gracias! De alguna manera me las arreglé para perder todo el punto de las pruebas unitarias (hasta ahora, practicaba pruebas PHP solo dentro de Code Dojo). También descubrí sobre wp_mock hoy, pero por alguna razón me las arreglo para ignorarlo. Lo que me molestó es que cualquier prueba, por pequeña que fuera, solía tardar al menos dos segundos en ejecutarse (cargue WP env primero, ejecute la prueba en segundo lugar). Gracias de nuevo por abrir mis ojos!
Ionut Staicu
44
Gracias @IonutStaicu Olvidé mencionar que no cargar WordPress hace que sus pruebas sean mucho más rápidas
gmazzap
66
También vale la pena señalar que el marco de prueba de la unidad WP Core es una herramienta increíble para ejecutar pruebas de INTEGRACIÓN, que serían pruebas automatizadas para garantizar que se integra bien con el propio WP (por ejemplo, no hay colisiones accidentales de nombres de funciones, etc.).
John P Bloch
1
@JohnPBloch +1 para un buen punto. Incluso si usar un espacio de nombres es suficiente para evitar cualquier colisión de nombres de funciones en WordPress, donde todo es global :) Pero, claro, las integraciones / pruebas funcionales son una cosa. Estoy jugando con Behat + Mink en este momento, pero todavía estoy practicando con eso.
gmazzap
1
Gracias por el "paseo en helicóptero" sobre el bosque UnitTest de WordPress - Todavía me estoy riendo de esa imagen épica ;-)
birgire