Testare i callback degli hook in WordPress
Sto sviluppando un plugin usando il TDD e una cosa che non riesco assolutamente a testare sono... gli hook.
Voglio dire, OK, posso testare il callback di un hook, ma come potrei testare se un hook viene effettivamente attivato (sia hook personalizzati che quelli predefiniti di WordPress)? Suppongo che un po' di mocking possa aiutare, ma semplicemente non riesco a capire cosa mi manchi.
Ho installato la test suite con WP-CLI. Secondo questa risposta, l'hook init
dovrebbe attivarsi, e invece... non lo fa; inoltre, il codice funziona correttamente all'interno di WordPress.
Da quello che ho capito, il bootstrap viene caricato per ultimo, quindi ha senso che non attivi init, quindi la domanda che rimane è: come diavolo dovrei testare se gli hook vengono attivati?
Grazie!
Il file bootstrap è questo:
$_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';
Il file testato è questo:
class RegisterCustomPostType {
function __construct()
{
add_action( 'init', array( $this, 'register_post_type' ) );
}
public function register_post_type()
{
register_post_type( 'foo' );
}
}
E il test stesso:
class CustomPostTypes extends WP_UnitTestCase {
function test_custom_post_type_creation()
{
$this->assertTrue( post_type_exists( 'foo' ) );
}
}
Grazie!

Testare in isolamento
Quando si sviluppa un plugin, il modo migliore per testarlo è senza caricare l'ambiente WordPress.
Se scrivi codice che può essere facilmente testato senza WordPress, il tuo codice diventa migliore.
Ogni componente che viene testato in modo unitario, dovrebbe essere testato in isolamento: quando testi una classe, devi testare solo quella specifica classe, assumendo che tutto il resto del codice funzioni perfettamente.
Questo è il motivo per cui i test unitari si chiamano "unit".
Come ulteriore vantaggio, senza caricare il core, il tuo test sarà molto più veloce.
Evitare gli hook nel costruttore
Un consiglio che posso darti è evitare di inserire hook nei costruttori. Questa è una delle cose che renderà il tuo codice testabile in isolamento.
Vediamo il codice di test nell'OP:
class CustomPostTypes extends WP_UnitTestCase {
function test_custom_post_type_creation() {
$this->assertTrue( post_type_exists( 'foo' ) );
}
}
E supponiamo che questo test fallisca. Chi è il colpevole?
- l'hook non è stato aggiunto affatto o non correttamente?
- il metodo che registra il post type non è stato chiamato affatto o con argomenti errati?
- c'è un bug in WordPress?
Come può essere migliorato?
Supponiamo che il codice della tua classe sia:
class RegisterCustomPostType {
function init() {
add_action( 'init', array( $this, 'register_post_type' ) );
}
public function register_post_type() {
register_post_type( 'foo' );
}
}
(Nota: farò riferimento a questa versione della classe per il resto della risposta)
Il modo in cui ho scritto questa classe ti permette di creare istanze della classe senza chiamare add_action
.
Nella classe sopra ci sono 2 cose da testare:
- il metodo
init
effettivamente chiamaadd_action
passandogli gli argomenti corretti - il metodo
register_post_type
effettivamente chiama la funzioneregister_post_type
Non ho detto che devi verificare se il post type esiste: se aggiungi l'azione corretta e se chiami register_post_type
, il custom post type deve esistere: se non esiste è un problema di WordPress.
Ricorda: quando testi il tuo plugin devi testare il tuo codice, non il codice di WordPress. Nei tuoi test devi assumere che WordPress (come qualsiasi altra libreria esterna che usi) funzioni bene. Questo è il significato di test unitario.
Ma... nella pratica?
Se WordPress non è caricato, se provi a chiamare i metodi della classe sopra, ottieni un errore fatale, quindi devi simulare le funzioni.
Il metodo "manuale"
Certamente puoi scrivere la tua libreria di mock o simulare "manualmente" ogni metodo. È possibile. Ti mostrerò come farlo, ma poi ti mostrerò un metodo più semplice.
Se WordPress non è caricato mentre i test sono in esecuzione, significa che puoi ridefinire le sue funzioni, ad esempio add_action
o register_post_type
.
Supponiamo che tu abbia un file, caricato dal tuo file di bootstrap, dove hai:
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();
}
Ho riscritto le funzioni per semplicemente aggiungere un elemento a un array globale ogni volta che vengono chiamate.
Ora dovresti creare (se non ne hai già uno) la tua classe base di test che estende PHPUnit_Framework_TestCase
: questo ti permette di configurare facilmente i tuoi test.
Può essere qualcosa come:
class Custom_TestCase extends \PHPUnit_Framework_TestCase {
public function setUp() {
$GLOBALS['counter'] = array();
}
}
In questo modo, prima di ogni test, il counter globale viene resettato.
E ora il tuo codice di test (mi riferisco alla classe riscritta che ho postato sopra):
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' ) );
}
}
Dovresti notare:
- Sono stato in grado di chiamare i due metodi separatamente e WordPress non è caricato affatto. In questo modo se un test fallisce, so esattamente chi è il colpevole.
- Come ho detto, qui testo che le classi chiamano le funzioni WP con gli argomenti previsti. Non c'è bisogno di testare se il CPT esiste davvero. Se stai testando l'esistenza del CPT, allora stai testando il comportamento di WordPress, non il comportamento del tuo plugin...
Bello.. ma è una rottura!
Sì, se devi simulare manualmente tutte le funzioni di WordPress, è davvero una seccatura. Un consiglio generale che posso dare è di usare il minor numero possibile di funzioni WP: non devi riscrivere WordPress, ma astrarre le funzioni WP che usi in classi personalizzate, in modo che possano essere simulate e facilmente testate.
Ad esempio, riguardo all'esempio sopra, puoi scrivere una classe che registra i post type, chiamando register_post_type
su 'init' con gli argomenti dati.
Con questa astrazione devi ancora testare quella classe, ma in altri punti del tuo codice che registrano post type puoi usare quella classe, simulandola nei test (quindi assumendo che funzioni).
La cosa fantastica è che se scrivi una classe che astrae la registrazione dei CPT, puoi creare un repository separato per essa, e grazie a strumenti moderni come Composer includerla in tutti i progetti dove ti serve: testa una volta, usa ovunque. E se mai trovi un bug, puoi correggerlo in un posto e con un semplice composer update
tutti i progetti dove è usata saranno corretti.
Per la seconda volta: scrivere codice che è testabile in isolamento significa scrivere codice migliore.
Ma prima o poi ho bisogno di usare funzioni WP da qualche parte...
Certo. Non dovresti mai agire in parallelo al core, non ha senso. Puoi scrivere classi che incapsulano le funzioni WP, ma quelle classi devono essere testate anche loro. Il metodo "manuale" descritto sopra può essere usato per compiti molto semplici, ma quando una classe contiene molte funzioni WP può essere una seccatura.
Fortunatamente, là fuori ci sono brave persone che scrivono cose buone. 10up, una delle più grandi agenzie WP, mantiene una libreria molto utile per chi vuole testare i plugin nel modo giusto. È WP_Mock
.
Ti permette di simulare funzioni e hook WP. Supponendo che tu l'abbia caricato nei tuoi test (vedi readme del repo), lo stesso test che ho scritto sopra diventa:
class CustomPostTypes extends Custom_TestCase {
function test_init() {
$r = new RegisterCustomPostType;
// testa che l'azione sia stata aggiunta con gli argomenti dati
\WP_Mock::expectActionAdded( 'init', array( $r, 'register_post_type' ) );
$r->init();
}
function test_register_post_type() {
// testa che la funzione sia stata chiamata con gli argomenti dati e eseguita una volta
\WP_Mock::wpFunction( 'register_post_type', array(
'times' => 1,
'args' => array( 'foo' ),
) );
$r = new RegisterCustomPostType;
$r->register_post_type();
}
}
Semplice, no? Questa risposta non è un tutorial per WP_Mock
, quindi leggi il readme del repo per maggiori informazioni, ma l'esempio sopra dovrebbe essere abbastanza chiaro, credo.
Inoltre, non devi scrivere nessuna funzione add_action
o register_post_type
simulata da te, o mantenere variabili globali.
E le classi WP?
WP ha anche alcune classi, e se WordPress non è caricato quando esegui i test, devi simularle.
Questo è molto più semplice che simulare funzioni, PHPUnit ha un sistema integrato per simulare oggetti, ma qui voglio suggerirti Mockery. È una libreria molto potente e facile da usare. Inoltre, è una dipendenza di WP_Mock
, quindi se lo hai hai anche Mockery.
Ma che dire di WP_UnitTestCase
?
La suite di test di WordPress è stata creata per testare il core di WordPress, e se vuoi contribuire al core è fondamentale, ma usarla per i plugin ti fa solo testare non in isolamento.
Guarda oltre il mondo WP: ci sono molti framework e CMS PHP moderni là fuori e nessuno di loro suggerisce di testare plugin/moduli/estensioni (o come vengono chiamati) usando il codice del framework.
Se ti mancano le factories, una funzionalità utile della suite, devi sapere che ci sono cose fantastiche là fuori.
Problemi e svantaggi
C'è un caso in cui il flusso di lavoro che ho suggerito qui manca: test personalizzati del database.
Infatti, se usi le tabelle standard di WordPress e funzioni per scriverci (a livello più basso i metodi di $wpdb
) non hai mai bisogno di effettivamente scrivere dati o testare se i dati sono effettivamente nel database, devi solo assicurarti che i metodi corretti siano chiamati con gli argomenti corretti.
Tuttavia, puoi scrivere plugin con tabelle personalizzate e funzioni che costruiscono query per scriverci, e testare se quelle query funzionano è tua responsabilità.
In quei casi la suite di test di WordPress può aiutarti molto, e caricare WordPress può essere necessario in alcuni casi per eseguire funzioni come dbDelta
.
(Non c'è bisogno di dire di usare un db diverso per i test, vero?)
Fortunatamente PHPUnit ti permette di organizzare i tuoi test in "suite" che possono essere eseguite separatamente, quindi puoi scrivere una suite per test personalizzati del database dove carichi l'ambiente WordPress (o parte di esso) lasciando tutti gli altri test senza WordPress.
Solo assicurati di scrivere classi che astraggano il maggior numero possibile di operazioni sul database, in modo che tutte le altre classi del plugin le usino, così usando mock puoi testare correttamente la maggior parte delle classi senza avere a che fare con il database.
Per la terza volta, scrivere codice facilmente testabile in isolamento significa scrivere codice migliore.

Accidenti, un sacco di informazioni utili! Grazie! In qualche modo mi sono perso il punto centrale dei test unitari (fino ad ora, praticavo i test PHP solo all'interno di Code Dojo). Avevo anche scoperto wp_mock oggi stesso, ma per qualche motivo l'ho ignorato.
Ciò che mi ha fatto arrabbiare è che qualsiasi test, per quanto piccolo, impiegava almeno due secondi per essere eseguito (prima caricava l'ambiente WP, poi eseguiva il test).
Grazie ancora per avermi aperto gli occhi!

Grazie @IonutStaicu, mi ero dimenticato di dire che non caricare WordPress rende i tuoi test molto più veloci

Esatto. Perché su Dojo avevo tempi di esecuzione inferiori a 1-200ms (pochi test, massimo 20-30); la suite di test di wp? bastava a farmi uscire dal flusso.

Vale anche la pena sottolineare che il framework di test unitari di WP Core è uno strumento straordinario per eseguire test di INTEGRAZIONE, che sarebbero test automatizzati per garantire che si integri bene con WP stesso (ad esempio, che non ci siano collisioni accidentali di nomi di funzioni, ecc.).

@JohnPBloch +1 per l'ottimo punto. Anche se usare un namespace è sufficiente per evitare qualsiasi collisione di nomi di funzioni in WordPress, dove tutto è globale :) Ma, certo, i test di integrazione/funzionali sono una cosa importante. Al momento sto giocando con Behat + Mink ma sto ancora facendo pratica con questi strumenti.

Grazie per il "giro in elicottero" sopra la foresta di UnitTest di WordPress - sto ancora ridendo per quell'immagine epica ;-)

@gmazzap Questa risposta sembra coprire molti dei problemi che mi stavano dando molti mal di testa. Tuttavia, o mi sfugge un punto, oppure non copre il caso in cui devo testare una classe che crea un'istanza di un'altra classe (ad esempio WP_Post): pensavo che factory-muffin potesse coprire questo caso, ma questo funziona quando l'istanza della classe viene passata come dipendenza. In questo caso, l'istanza della classe viene creata all'interno della classe che sto testando.

@andrea-sciamanna il problema è che un codice "testabile" ben scritto dovrebbe evitare di creare istanze di oggetti all'interno di altri oggetti (a meno che non siano oggetti PHP come ArrayObject
o value objects), proprio per questo motivo. Se questo accade, e non puoi/vuoi modificare il codice, l'unico modo per testare "unitariamente" quegli oggetti è creare degli "stub". Ad esempio, puoi scrivere una classe WP_Post
personalizzata, che viene caricata solo nei test. Non è facile da mantenere, ed è per questo che scrivere codice pensando ai test, o addirittura scrivere i test prima del codice (TDD) è una buona cosa :)

Penso che questo meriti una domanda separata, soprattutto perché non c'è abbastanza spazio per continuare a scrivere qui :) Domanda: http://wordpress.stackexchange.com/questions/233132/unit-tests-dealing-with-dependencies
