Testarea callback-urilor de hook-uri
Dezvolt un plugin folosind TDD și nu reușesc să testez corect... hook-urile.
Adică OK, pot testa callback-ul unui hook, dar cum pot testa dacă un hook se declanșează efectiv (atât hook-uri custom cât și cele default din WordPress)? Presupun că ar trebui să folosesc mocking, dar nu reușesc să înțeleg ce lipsește.
Am instalat suita de teste cu WP-CLI. Conform acestui răspuns, hook-ul init
ar trebui să se declanșeze, dar... nu o face; totuși, codul funcționează în interiorul WordPress.
Din câte înțeleg, bootstrap-ul se încarcă ultimul, deci are sens să nu declanșeze init, așa că întrebarea rămâne: cum naiba ar trebui să testez dacă hook-urile se declanșează?
Mulțumesc!
Fișierul bootstrap arată astfel:
$_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';
Fișierul testat arată astfel:
class RegisterCustomPostType {
function __construct()
{
add_action( 'init', array( $this, 'register_post_type' ) );
}
public function register_post_type()
{
register_post_type( 'foo' );
}
}
Și testul în sine:
class CustomPostTypes extends WP_UnitTestCase {
function test_custom_post_type_creation()
{
$this->assertTrue( post_type_exists( 'foo' ) );
}
}
Mulțumesc!

Testare în izolare
Când dezvolți un plugin, cel mai bun mod de a-l testa este fără a încărca mediul WordPress.
Dacă scrii cod care poate fi testat ușor fără WordPress, codul tău devine mai bun.
Fiecare componentă care este testată unitar, ar trebui testată în izolare: când testezi o clasă, trebuie să testezi doar acea clasă specifică, presupunând că restul codului funcționează perfect.
Acesta este motivul pentru care testele unitare se numesc "unitare".
Ca un beneficiu suplimentar, fără a încărca nucleul, testele tale vor rula mult mai rapid.
Evită hook-urile în constructor
Un sfat pe care ți-l pot da este să eviți să pui hook-uri în constructori. Acesta este unul dintre lucrurile care vor face codul tău testabil în izolare.
Să vedem codul de test din OP:
class CustomPostTypes extends WP_UnitTestCase {
function test_custom_post_type_creation() {
$this->assertTrue( post_type_exists( 'foo' ) );
}
}
Și să presupunem că acest test eșuează. Cine este vinovatul?
- hook-ul nu a fost adăugat deloc sau nu a fost adăugat corect?
- metoda care înregistrează tipul de post nu a fost apelată deloc sau cu argumente greșite?
- există o eroare în WordPress?
Cum poate fi îmbunătățit?
Să presupunem că codul clasei tale este:
class RegisterCustomPostType {
function init() {
add_action( 'init', array( $this, 'register_post_type' ) );
}
public function register_post_type() {
register_post_type( 'foo' );
}
}
(Notă: voi face referire la această versiune a clasei pentru restul răspunsului)
Modul în care am scris această clasă îți permite să creezi instanțe ale clasei fără a apela add_action
.
În clasa de mai sus sunt 2 lucruri de testat:
- metoda
init
apelează efectivadd_action
transmitându-i argumentele corecte - metoda
register_post_type
apelează efectiv funcțiaregister_post_type
Nu am spus că trebuie să verifici dacă tipul de post există: dacă adaugi acțiunea corectă și dacă apelezi register_post_type
, tipul de post personalizat trebuie să existe: dacă nu există, este o problemă a WordPress.
Reține: când testezi pluginul tău, trebuie să testezi codul tău, nu codul WordPress. În testele tale trebuie să presupui că WordPress (la fel ca orice altă bibliotecă externă pe care o folosești) funcționează corect. Acesta este sensul testului unitar.
Dar... în practică?
Dacă WordPress nu este încărcat, dacă încerci să apelezi metodele clasei de mai sus, vei primi o eroare fatală, așa că trebuie să mock-uiți funcțiile.
Metoda "manuală"
Sigur că poți să-ți scrii propria bibliotecă de mocking sau să mock-uiți manual fiecare metodă. Este posibil. Îți voi arăta cum să faci asta, dar apoi voi arăta o metodă mai ușoară.
Dacă WordPress nu este încărcat în timpul testelor, înseamnă că poți redefini funcțiile sale, de ex. add_action
sau register_post_type
.
Să presupunem că ai un fișier, încărcat din fișierul tău bootstrap, în care ai:
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();
}
Am rescris funcțiile pentru a adăuga pur și simplu un element într-un array global de fiecare dată când sunt apelate.
Acum ar trebui să creezi (dacă nu ai deja) propria clasă de bază pentru teste care extinde PHPUnit_Framework_TestCase
: asta îți permite să configurezi ușor testele tale.
Poate fi ceva de genul:
class Custom_TestCase extends \PHPUnit_Framework_TestCase {
public function setUp() {
$GLOBALS['counter'] = array();
}
}
În acest fel, înainte de fiecare test, contorul global este resetat.
Și acum codul tău de test (mă refer la clasa rescrisă pe care am postat-o mai sus):
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' ) );
}
}
Ar trebui să observi:
- Am putut apela cele două metode separat și WordPress nu este încărcat deloc. În acest fel, dacă un test eșuează, știu exact cine este vinovatul.
- După cum am spus, aici testez că clasele apelează funcțiile WP cu argumentele așteptate. Nu este nevoie să testez dacă CPT există cu adevărat. Dacă testezi existența CPT-ului, atunci testezi comportamentul WordPress, nu comportamentul pluginului tău...
Frumos.. dar e o corvoadă!
Da, dacă trebuie să mock-uiți manual toate funcțiile WordPress, este cu adevărat o corvoadă. Un sfat general pe care ți-l pot da este să folosești cât mai puține funcții WP posibil: nu trebuie să rescrii WordPress, ci să abstractizezi funcțiile WP pe care le folosești în clase personalizate, astfel încât să poată fi mock-uite și testate ușor.
De exemplu, în cazul de mai sus, poți scrie o clasă care înregistrează tipuri de post, apelând register_post_type
pe 'init' cu argumentele date.
Cu această abstractizare, tot va trebui să testezi acea clasă, dar în alte părți ale codului tău care înregistrează tipuri de post, poți folosi acea clasă, mock-uind-o în teste (presupunând că funcționează).
Lucrul minunat este că, dacă scrii o clasă care abstractizează înregistrarea CPT-urilor, poți crea un repository separat pentru ea și, datorită uneltelor moderne precum Composer, să o incluzi în toate proiectele în care ai nevoie de ea: testează o dată, folosește peste tot. Și dacă găsești vreodată un bug în ea, îl poți repara într-un singur loc și cu un simplu composer update
toate proiectele în care este folosită vor fi reparate.
Pentru a doua oară: a scrie cod care este testabil în izolare înseamnă a scrie cod mai bun.
Dar mai devreme sau mai târziu trebuie să folosesc funcții WP undeva...
Desigur. Nu ar trebui niciodată să acționezi în paralel cu nucleul, nu are sens. Poți scrie clase care învelesc funcțiile WP, dar acele clase trebuie testate și ele. Metoda "manuală" descrisă mai sus poate fi folosită pentru sarcini foarte simple, dar când o clasă conține multe funcții WP, poate fi o corvoadă.
Din fericire, există oameni buni care scriu lucruri bune. 10up, una dintre cele mai mari agenții WordPress, menține o bibliotecă foarte bună pentru oamenii care doresc să testeze pluginurile corect. Este WP_Mock
.
Îți permite să mock-uiți funcțiile și hook-urile WP. Presupunând că ai încărcat în testele tale (vezi readme-ul repo-ului) același test pe care l-am scris mai sus devine:
class CustomPostTypes extends Custom_TestCase {
function test_init() {
$r = new RegisterCustomPostType;
// testează că acțiunea a fost adăugată cu argumentele date
\WP_Mock::expectActionAdded( 'init', array( $r, 'register_post_type' ) );
$r->init();
}
function test_register_post_type() {
// testează că funcția a fost apelată cu argumentele date și a rulat o dată
\WP_Mock::wpFunction( 'register_post_type', array(
'times' => 1,
'args' => array( 'foo' ),
) );
$r = new RegisterCustomPostType;
$r->register_post_type();
}
}
Simplu, nu-i așa? Acest răspuns nu este un tutorial pentru WP_Mock
, așa că citește readme-ul repo-ului pentru mai multe informații, dar exemplul de mai sus ar trebui să fie destul de clar, cred.
Mai mult, nu trebuie să scrii niciun add_action
sau register_post_type
mock-uit de mână sau să menții variabile globale.
Și clasele WP?
WP are și câteva clase, iar dacă WordPress nu este încărcat când rulezi teste, trebuie să le mock-uiți.
Asta este mult mai ușor decât mock-uitul funcțiilor, PHPUnit are un sistem încorporat pentru mock-uitul obiectelor, dar aici vreau să-ți sugerez Mockery. Este o bibliotecă foarte puternică și foarte ușor de folosit. Mai mult, este o dependință a WP_Mock
, așa că dacă îl ai, ai și Mockery.
Dar ce zici de WP_UnitTestCase
?
Suita de teste WordPress a fost creată pentru a testa nucleul WordPress, iar dacă vrei să contribui la nucleu, este esențială, dar folosirea ei pentru pluginuri doar te face să nu testezi în izolare.
Uită-te dincolo de lumea WP: există o mulțime de framework-uri și CMS-uri moderne PHP și niciunul nu sugerează testarea pluginurilor/modulelor/extensiilor (sau cum or fi ele numite) folosind codul framework-ului.
Dacă îți lipsesc fabricile, o caracteristică utilă a suitei, trebuie să știi că există lucruri minunate pe acolo.
Capcane și dezavantaje
Există un caz în care fluxul de lucru pe care l-am sugerat aici are deficiențe: testarea bazei de date personalizate.
De fapt, dacă folosești tabele standard WordPress și funcții pentru a scrie acolo (la cel mai scăzut nivel metodele $wpdb
) nu trebuie niciodată să scrii efectiv date sau să testezi dacă datele sunt efectiv în baza de date, doar să te asiguri că metodele corecte sunt apelate cu argumentele corecte.
Cu toate acestea, poți scrie pluginuri cu tabele personalizate și funcții care construiesc interogări pentru a scrie acolo, iar testarea dacă acele interogări funcționează este responsabilitatea ta.
În acele cazuri, suita de teste WordPress te poate ajuta foarte mult, iar încărcarea WordPress poate fi necesară în unele cazuri pentru a rula funcții precum dbDelta
.
(Nu este nevoie să spunem să folosești o bază de date diferită pentru teste, nu-i așa?)
Din fericire, PHPUnit îți permite să organizezi testele în "suite" care pot fi rulate separat, astfel încât poți scrie un suit pentru teste personalizate de bază de date unde încarci mediul WordPress (sau o parte din el) lăsând restul testelor tale fără WordPress.
Doar asigură-te să scrii clase care abstractizează cât mai multe operații pe baza de date posibil, într-un mod în care toate celelalte clase ale pluginului le folosesc, astfel încât folosind mock-uri să poți testa corect majoritatea claselor fără a te ocupa de baza de date.
Pentru a treia oară, a scrie cod ușor de testat în izolare înseamnă a scrie cod mai bun.

Doamne, ce multe informații utile! Mulțumesc! Cumva am reușit să ratez toată esența testării unitare (până acum, am practicat testarea PHP doar în cadrul Code Dojo). Am aflat și despre wp_mock mai devreme astăzi, dar din anumite motive am reușit să-l ignor.
Ce m-a enervat este că orice test, indiferent cât de mic era, dura cel puțin două secunde să ruleze (încărcarea mediului WP mai întâi, executarea testului apoi).
Mulțumesc din nou că mi-ai deschis ochii!

Mulțumesc @IonutStaicu Am uitat să menționez că neloadarea WordPress face ca testele tale să fie mult mai rapide

Într-adevăr. Pentru că pe Dojo aveam un timp de rulare de sub 1-200ms (câteva teste, maxim 20-30); suita de teste wp? suficient cât să mă scoată din flow.

De asemenea, merită menționat că cadrul de testare unitară al WP Core este un instrument uimitor pentru rularea testelor de INTEGRARE, care ar fi teste automatizate pentru a se asigura că se integrează bine cu WP în sine (de exemplu, nu există coliziuni accidentale de nume de funcții, etc.).

@JohnPBloch +1 pentru punctul de vedere corect. Chiar dacă utilizarea unui namespace este suficientă pentru a evita orice coliziune de nume de funcții în WordPress, unde totul este global :) Dar, desigur, testele de integrare / funcționale sunt importante. În prezent mă joc cu Behat + Mink, dar încă exersez cu acestea.

Mulțumesc pentru "turul cu elicopterul" peste pădurea de UnitTest din WordPress - încă râd la acea imagine epica ;-)

@gmazzP Acest răspuns pare să acopere o mulțime dintre problemele care mi-au provocat multe dureri de cap. Totuși, fie că îmi scapă un punct, fie că nu acoperă cazul în care trebuie să testez o clasă care creează o instanță a altei clase (de exemplu WP_Post): credeam că factory-muffin ar putea acoperi acest lucru, dar asta funcționează când instanța clasei este transmisă ca dependență. În acest caz, instanța clasei este creată în interiorul clasei pe care o testez.

@andrea-sciamanna problema este că codul "testabil" bun ar trebui scris evitând crearea de instanțe de obiecte în interiorul altor obiecte (cu excepția cazului în care sunt obiecte PHP precum ArrayObject
sau obiecte de valoare), tocmai din acest motiv. Dacă se întâmplă acest lucru și nu poți sau nu vrei să modifici codul, singura modalitate de a testa "unit" acele obiecte este să creezi "stub-uri". De exemplu, poți scrie o clasă personalizată WP_Post
, care este încărcată doar în teste. Acest lucru nu este ușor de întreținut, iar de aceea scrierea de cod având în vedere testele sau chiar scrierea testelor înainte de cod (TDD) este un lucru bun :)

Cred că acest lucru merită o întrebare separată, mai ales pentru că nu există suficient spațiu pentru a continua să scriu aici :) Întrebare: http://wordpress.stackexchange.com/questions/233132/unit-tests-dealing-with-dependencies
