Cum se face mock la o funcție în PHP?

Am următoarea problemă: vreau să testez niște clase ce sunt folosite într-un plugin de WordPress, iar aceste clase folosesc funcții din core-ul WP (e.g. add_post_meta, add_action etc).

Soluția este (cred) să fac mock la aceste funcții (cel puțin așa am rezolvat problema în JavaScript), doar că… nu prea ințeleg cum aș putea face acest lucru, toate articolele găsite tratând doar situația în care se folosesc (exclusiv) clase.

Idei? Sugestii?


Edit: o soluție ar fi să scriu pur și simplu funcțiile respective ori într-un fișier separat (un fel de „fixtures”) ori înainte de a defini clasa ce conține testele. Dar nu știu cum aș putea testa mai departe ce întoarce fiecare funcție (cu mock ar fi simplu să „emulez” diferite comportamente)

1 Like

Eu n-am inteles, poti sa dai un exemplu cum vrei sa testezi?

Sigur. În codul plugin-ului am așa (am eliminat lucrurile ce nu sunt importante):

$existing_state = term_exists( $state, 'job_location' );
if( !$existing_state ){
    $state = wp_insert_term( $state, 'job_location' );
    $locations[] = $state->term_id;
} else {
    $locations[] = $existing_state['term_id'];
}

Eh, în cazul de față, aș avea nevoie să fac mock de două ori pentru term_exists, astfel încât să acopăr toate cazurile (ceea ce înseamnă că cele două mock-uri vor trebui să întoarcă două lucruri diferite: un stdClass respectiv un WP_Error).


Să zicem că am rezolvat parțial folosind WP_Mock:

    $a = new stdClass();
    $a->term_id = 1;
    \WP_Mock::wpFunction('wp_insert_term', array('times' => '0-100', 'return' => $a) );

Problema este că testele au devenit extrem de lente. Durează aproximativ 20 secunde să facă un singur assert.


M-am mai gândit la o variantă, dar cred că este extrem: o clasă statică cu câte o metodă pentru fiecare funcție folosită în WP:

class WP_Wrapper {
    public static function wp_insert_term(){}
    public static function term_exists(){}
}

Astfel încât să pot folosi în exemplul de mai sus WP_Wrapper::term_exists().

Ai putea sa faci in test un mock:

rename_function('term_exists', 'wp_term_exists');
override_function('term_exists', '$a', '$b = $a + 1; return $b;');

term_exists va fi un mock, iar wp_term_exists il poti folosii unde ai nevoie de functionalitatea originala (in caz ca ai)

1 Like

@iamntz Eu folosesc metoda cu clasa separata (pe care ai descris-o si tu).
Tu vrei ca pluginul tau sa fie testabil deci trebuie sa faci tot posibilul sa-ti asiguri mock-uirea acelor functii specifice Wordpress.

In plugin-ul tau poti folosi o clasa \MyPlugin\Functions

<?php
namespace MyPlugin;
class Functions {
 public static function wp_insert_term( $term, $taxonomy, $args = array() ) {
  return \wp_insert_term($term, $taxonomy, $args = array());
 }
}

Tu in cod-ul plugin-ului folosesti:

<?php
use \MyPlugin\Functions as F;

F\wp_insert_term();
?>

In testul tau e simplu sa mock-uiesti intreaga clasa Functions ca un mare bo$$:

    $FunctionsMock = $this->getMock('MyPlugin\Functions', array(
                                                                'wp_insert_term',
                                                                'wp_pix'
                                                            ));
    // Return a valid term id.
    $FunctionsMock->expects($this->any())
        ->method('wp_insert_term')
        ->will($this->returnValue(123));

Am folosit in trecut runkit pentru override si am avut probleme - imi scapa acum de ce. Mi se pare foarte explicit asa cu o clasa separata. Atat codul cat si testul sunt explicite pentru oricine citeste.
Nu stiu care sunt implicatiile folosirii functiilor APD.

2 Likes

În final am făcut cam așa:

class CMS_Wrapper {
    function __construct() {}

    public function insert_post()
    {
        $args = func_get_args();
        return call_user_func_array( 'wp_insert_post', $args );
    }
}

Cred că e bine; cel puțin teoretic, cu o reimplementare a CMS_Wrapper aș putea folosi plugin-ul propriu-zis și în Drupal, Joomla sau ce CMS-uri or mai fi la modă :slight_smile:

Sunt si eu pentru aceasta varianta a lui @serbanghita . Se numeste design pattern-ul “Adaptor”. Adica iti faci o clasa care adapteaza call-urile tale la cele ale librariei 3rd party, in cazul tau functii builtin wp.

Ca un bonus al acestei solutii, iti vei putea raspunde foarte usor la intrebarea “Ce vreau sa testez?”. De fapt, daca porneai de la ceasta intrebarea iti dadeai seama ca ai un mix de cod cu responsabilitati diferite (vezi Single Responsibility Principle). Codul tau era responsabil si cu comunicarea cu wp, si cu efectuarea logicii ce vrei sa testezi. Daca mergeai pe directia asta, probabil ti-ai fi extras clase cu logica. Ai fi avut clasa cu pluginul tau, sa-i zicem “MyPlugin” care continea doar layer-ul de comunicare, si iti faceai clase cu logica ce vrei de fapt sa realizez. Aceasta abordare are avantajul ca te forteaza sa iti creezi clase ce abstractizeaza business logic-ul tau. Solutia sugerata mi sus merge in directia inversa, introduce un “wrapper”, si pierzi acest bonus de abstractizare.

1 Like