Test callback di hook


34

Sto sviluppando un plugin usando TDD e una cosa che non riesco completamente a testare sono ... ganci.

Voglio dire OK, posso testare il callback hook, ma come posso verificare se un hook si innesca effettivamente (sia hook personalizzati che hook predefiniti di WordPress)? Suppongo che un po 'di derisione possa aiutare, ma semplicemente non riesco a capire cosa mi sto perdendo.

Ho installato la suite di test con WP-CLI. Secondo questa risposta , inithook dovrebbe innescarsi, eppure ... non lo fa; inoltre, il codice funziona all'interno di WordPress.

Da quanto ho capito, il bootstrap è stato caricato per ultimo, quindi ha senso non attivare init, quindi la domanda che rimane è: come diamine dovrei testare se gli hook sono attivati?

Grazie!

Il file bootstrap è simile al seguente:

$_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 è simile al seguente:

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!


Se stai correndo phpunit, riesci a vedere i test falliti o superati? Hai installato bin/install-wp-tests.sh?
Sven,

Penso che parte del problema sia che forse RegisterCustomPostType::__construct()non viene mai chiamato quando il plug-in viene caricato per i test. È anche possibile che tu sia interessato dal bug # 29827 ; magari prova ad aggiornare la tua versione della suite di unit test di WP.
JD,

@Sven: sì, i test falliscono; ho installato bin/install-wp-tests.sh(da quando ho usato wp-cli) @JD: viene chiamato il costrutto RegisterCustomPostType :: __ (appena aggiunta die()un'istruzione e phpunit si ferma lì)
Ionut Staicu,

Non sono troppo sicuro dal punto di vista dei test unitari (non del mio forte), ma dal punto di vista letterale puoi usare did_action()per verificare se le azioni sono state attivate.
Rarst

@Rarst: grazie per il suggerimento, ma non funziona ancora. Per qualche ragione, penso che i tempi siano sbagliati (i test vengono eseguiti prima initdell'hook).
Ionut Staicu,

Risposte:


72

Test in isolamento

Quando si sviluppa un plug-in, 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 sottoposto a test unitario deve essere testato separatamente : quando si verifica una classe, è necessario testare solo quella classe specifica, assumendo che tutto il resto del codice funzioni perfettamente.

The Isolator

Questo è il motivo per cui i test unitari sono chiamati "unit".

Come ulteriore vantaggio, senza caricare il core, il test verrà eseguito molto più velocemente.

Evitare ganci nel costruttore

Un consiglio che posso darti è di evitare di mettere ganci nei costruttori. Questa è una delle cose che renderà il tuo codice testabile da solo.

Vediamo il codice di prova in 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 ?

  • il gancio non è stato aggiunto affatto o non correttamente?
  • il metodo che registra il tipo di post non è stato chiamato affatto o con argomenti sbagliati?
  • 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 consente di creare istanze della classe senza chiamare add_action.

Nella classe sopra ci sono 2 cose da testare:

  • il metodo in init realtà chiama add_actionpassando gli argomenti propri
  • il metodo chiama register_post_type effettivamente laregister_post_type funzione

Non ho detto che devi controllare se esiste il tipo di post: se aggiungi l'azione corretta e se chiami register_post_type, il tipo di post personalizzato deve esistere: se non esiste è un problema di WordPress.

Ricorda: quando provi il tuo plugin devi testare il tuo codice, non il codice WordPress. Nei tuoi test devi presumere che WordPress (proprio come qualsiasi altra libreria esterna che usi) funzioni bene. Questo è il significato di unit test.

Ma ... in pratica?

Se WordPress non viene caricato, se si tenta di chiamare i metodi di classe sopra, si ottiene un errore irreversibile, quindi è necessario prendere in giro le funzioni.

Il metodo "manuale"

Sicuramente puoi scrivere la tua libreria beffarda o deridere "manualmente" ogni metodo. È possibile. Ti dirò come farlo, ma poi ti mostrerò un metodo più semplice.

Se WordPress non è caricato mentre i test sono in esecuzione, significa che è possibile ridefinire le sue funzioni, ad esempio add_actiono register_post_type.

Supponiamo che tu abbia un file, caricato dal tuo file 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 aggiungere semplicemente un elemento a un array globale ogni volta che vengono chiamate.

Ora dovresti creare (se non ne hai già uno) estendere la tua classe di test case di base PHPUnit_Framework_TestCase: ciò ti consente di configurare facilmente i tuoi test.

Può essere qualcosa del tipo:

class Custom_TestCase extends \PHPUnit_Framework_TestCase {

    public function setUp() {
        $GLOBALS['counter'] = array();
    }

}

In questo modo, prima di ogni test, il contatore globale viene resettato.

E ora il tuo codice di prova (mi riferisco alla classe riscritta che ho pubblicato 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 è stato caricato affatto. In questo modo se un test fallisce, so esattamente chi è il colpevole.
  • Come ho detto, qui provo che le classi chiamano funzioni WP con argomenti previsti. Non è necessario verificare se esiste davvero CPT. Se stai testando l'esistenza di CPT, stai testando il comportamento di WordPress, non il comportamento del plug-in ...

Bello .. ma è una PITA!

Sì, se devi deridere 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 astratto funzioni WP che usi in classi personalizzate, in modo che possano essere derise e testate facilmente.

Ad esempio, per quanto riguarda l'esempio sopra, puoi scrivere una classe che registra i tipi di posta, chiamando register_post_type "init" con argomenti specifici. Con questa astrazione devi ancora testare quella classe, ma in altre parti del tuo codice che registrano i tipi di post puoi usare quella classe, deridendola nei test (quindi supponendo che funzioni).

La cosa fantastica è che, se scrivi una classe che estrae la registrazione CPT, puoi creare un repository separato per esso, e grazie a strumenti moderni come Composer lo incorpora in tutti i progetti in cui ne hai bisogno: prova una volta, usa ovunque . E se trovi mai un bug in esso, puoi risolverlo in un posto e con un semplice anche composer updatetutti i progetti in cui viene utilizzato vengono corretti.

Per la seconda volta: scrivere codice testabile in isolamento significa scrivere codice migliore.

Ma prima o poi devo usare le funzioni WP da qualche parte ...

Ovviamente. Non dovresti mai agire in parallelo con il core, non ha senso. Puoi scrivere classi che racchiudono funzioni WP, ma anche quelle classi devono essere testate. Il metodo "manuale" sopra descritto può essere usato per compiti molto semplici, ma quando una classe contiene molte funzioni WP può essere una seccatura.

Fortunatamente laggiù ci sono persone buone che scrivono cose buone. 10up , una delle più grandi agenzie del WP, mantiene un'ottima libreria per le persone che vogliono testare i plugin nel modo giusto. Lo è WP_Mock.

Ti permette di deridere funzioni WP e ganci . Supponendo che tu abbia caricato nei tuoi test (vedi il file Leggimi repo) lo stesso test che ho scritto sopra diventa:

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

}

Semplice, no? Questa risposta non è un tutorial per WP_Mock, quindi leggi il readme del repository per maggiori informazioni, ma l'esempio sopra dovrebbe essere abbastanza chiaro, credo.

Inoltre, non è necessario scrivere alcun beffardo add_actiono register_post_typeda solo o mantenere variabili globali.

E le classi WP?

WP ha anche alcune classi e se WordPress non viene caricato quando si eseguono i test, è necessario prenderli in giro.

È molto più facile delle funzioni di derisione, PHPUnit ha un sistema incorporato per deridere oggetti, ma qui voglio suggerirti derisione . È una libreria molto potente e molto facile da usare. Inoltre, è una dipendenza diWP_Mock , quindi se ce l'hai hai anche la beffa.

Ma che dire WP_UnitTestCase?

La suite di test di WordPress è stata creata per testare il core di WordPress e, se si desidera contribuire al core, è fondamentale, ma l'utilizzo per i plug-in ti fa solo provare non in isolamento.

Metti gli occhi sul mondo WP: ci sono molti moderni framework PHP e CMS là fuori e nessuno di loro suggerisce di testare plugin / moduli / estensioni (o come si chiamano) usando il codice del framework.

Se ti mancano le fabbriche, una caratteristica utile della suite, devi sapere che ci sono cose fantastiche laggiù.

Gotcha e aspetti negativi

C'è un caso in cui manca il flusso di lavoro che ho suggerito qui: test del database personalizzato .

In effetti, se si utilizzano le tabelle di WordPress standard e funzioni di scrivere lì (al livello più basso $wpdbmetodi) non è mai necessario in realtà i dati di scrittura o di prova se i dati sono in realtà nel database, tanto per essere sicuro che i metodi corretti vengono chiamati con argomenti corretti.

Tuttavia, puoi scrivere plug-in con tabelle e funzioni personalizzate che creano query da scrivere lì e testare se quelle query funzionano è tua responsabilità.

In questi casi, la suite di test di WordPress può aiutarti molto e in alcuni casi potrebbe essere necessario caricare WordPress per eseguire funzioni come dbDelta.

(Non c'è bisogno di dire di usare un db diverso per i test, non è vero?)

Fortunatamente PHPUnit ti consente di organizzare i tuoi test in "suite" che possono essere eseguite separatamente, in modo da poter scrivere una suite per test di database personalizzati in cui carichi l'ambiente WordPress (o parte di esso) lasciando tutto il resto dei tuoi test senza WordPress .

Assicurati di scrivere solo le classi che astraggono il maggior numero possibile di operazioni di database, in modo che tutte le altre classi di plugin ne facciano uso, in modo che usando i mock puoi testare correttamente la maggior parte delle classi senza occuparsi del database.

Per la terza volta, scrivere codice facilmente testabile in isolamento significa scrivere codice migliore.


5
Santa merda, molte informazioni utili! Grazie! In qualche modo sono riuscito a perdere l'intero punto del test unitario (fino ad ora praticavo i test PHP solo all'interno del Code Dojo). Oggi ho anche scoperto wp_mock, ma per qualche motivo riesco a ignorarlo. Ciò che mi ha fatto incazzare è che qualsiasi test, non importa quanto fosse piccolo, impiegava almeno due secondi per essere eseguito (carica prima WP env, esegui il test secondo). Grazie ancora per avermi aperto gli occhi!
Ionut Staicu,

4
Grazie @IonutStaicu Ho dimenticato di menzionare che il mancato caricamento di WordPress rende i test molto più veloci
gmazzap

6
Vale anche la pena sottolineare che il framework di test dell'unità WP Core è uno strumento straordinario per l'esecuzione dei test di INTEGRAZIONE, che sarebbe test automatizzati per garantire che si integri bene con il WP stesso (ad es. Non ci sono collisioni accidentali di nomi di funzioni, ecc.).
John P Bloch,

1
@JohnPBloch +1 per un buon punto. Anche se l'utilizzo di uno spazio dei nomi è sufficiente per evitare qualsiasi collisione di nomi di funzioni in WordPress, dove tutto è globale :) Ma, sicuramente, integrazioni / test funzionali sono una cosa. Al momento sto giocando con Behat + Mink, ma mi sto ancora esercitando.
gmazzap

1
Grazie per il "giro in elicottero" sulla foresta UnitTest di WordPress - Sto ancora ridendo di quell'immagine epica ;-)
Birgire,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.