Test des rappels de crochets

34

Je développe un plugin utilisant TDD et une chose que je ne parviens pas à tester est ... les hooks.

Je veux dire OK, je peux tester le rappel de hook, mais comment puis-je tester si un hook se déclenche réellement (les hooks personnalisés et les hooks par défaut de WordPress)? Je suppose que quelques moqueries aideront, mais je ne peux tout simplement pas comprendre ce qui me manque.

J'ai installé la suite de tests avec WP-CLI. Selon cette réponse , inithook devrait se déclencher, mais ... ce n'est pas le cas; De plus, le code fonctionne dans WordPress.

De ma compréhension, le bootstrap est chargé en dernier, il est donc logique de ne pas déclencher init, la question qui reste est donc: comment diable devrais-je tester si des hooks sont déclenchés?

Merci!

Le fichier de démarrage ressemble à ceci:

$_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';

Le fichier testé ressemble à ceci:

class RegisterCustomPostType {
  function __construct()
  {
    add_action( 'init', array( $this, 'register_post_type' ) );
  }

  public function register_post_type()
  {
    register_post_type( 'foo' );
  }
}

Et le test lui-même:

class CustomPostTypes extends WP_UnitTestCase {
  function test_custom_post_type_creation()
  {
    $this->assertTrue( post_type_exists( 'foo' ) );
  }
}

Merci!

Ionut Staicu
la source
Si vous êtes en cours d'exécution phpunit, pouvez-vous voir des tests ayant échoué ou réussi? Avez-vous installé bin/install-wp-tests.sh?
Sven
Je pense qu'une partie du problème est que peut-être RegisterCustomPostType::__construct()n'est jamais appelé lorsque le plug-in est chargé pour les tests. Il est également possible que vous soyez affecté par le bogue n ° 29827 ; essayez peut-être de mettre à jour votre version de la suite de tests unitaires de WP.
JD
@Sven: oui, les tests échouent; j'ai installé bin/install-wp-tests.sh(depuis que j'ai utilisé wp-cli) @JD: RegisterCustomPostType :: __ construct est appelé (vient d'ajouter une die()déclaration et phpunit s'arrête là)
Ionut Staicu
Je ne suis pas très sûr du côté des tests unitaires (ce n'est pas mon point fort), mais d'un point de vue littéral, vous pouvez utiliser did_action()pour vérifier si des actions ont été déclenchées.
Rarst
@Rarst: merci pour la suggestion, mais cela ne fonctionne toujours pas. Pour une raison quelconque, je pense que le timing est erroné (les tests sont exécutés avant le inithook).
Ionut Staicu

Réponses:

72

Test en isolation

Lors du développement d'un plugin, le meilleur moyen de le tester est de ne pas charger l'environnement WordPress.

Si vous écrivez du code qui peut être facilement testé sans WordPress, votre code devient meilleur .

Chaque composant testé par unité doit être testé séparément : lorsque vous testez une classe, vous devez uniquement tester cette classe spécifique, en supposant que tout le code fonctionne parfaitement.

L'isolateur

C'est la raison pour laquelle les tests unitaires sont appelés "unit".

Un avantage supplémentaire, sans chargement de noyau, votre test s'exécutera beaucoup plus rapidement.

Éviter les crochets dans le constructeur

Un conseil que je peux vous donner est d'éviter de mettre des crochets dans les constructeurs. C'est l'une des choses qui rendra votre code testable de manière isolée.

Voyons le code de test dans OP:

class CustomPostTypes extends WP_UnitTestCase {
  function test_custom_post_type_creation() {
    $this->assertTrue( post_type_exists( 'foo' ) );
  }
}

Et supposons que ce test échoue . Qui est le coupable ?

  • le crochet n'a pas été ajouté du tout ou pas correctement?
  • la méthode qui enregistre le type de publication n’a pas été appelée du tout ou avec de mauvais arguments?
  • il y a un bug dans WordPress?

Comment cela peut-il être amélioré?

Supposons que votre code de classe est:

class RegisterCustomPostType {

  function init() {
    add_action( 'init', array( $this, 'register_post_type' ) );
  }

  public function register_post_type() {
    register_post_type( 'foo' );
  }
}

(Remarque: je vais me référer à cette version de la classe pour le reste de la réponse.)

La façon dont j'ai écrit cette classe vous permet de créer des instances de la classe sans appeler add_action.

Dans la classe ci-dessus, il y a 2 choses à tester:

  • la méthode appelle init en fait en lui add_actionpassant des arguments appropriés
  • la méthode appelle register_post_type réellement laregister_post_type fonction

Je n'ai pas dit que vous deviez vérifier si le type de message existe: si vous ajoutez l'action appropriée et si vous appelez register_post_type, le type de message personnalisé doit exister: s'il n'existe pas, il s'agit d'un problème de WordPress.

Rappelez-vous: lorsque vous testez votre plugin, vous devez tester votre code, pas le code WordPress. Dans vos tests, vous devez supposer que WordPress (comme toute autre bibliothèque externe que vous utilisez) fonctionne bien. C'est le sens du test unitaire .

Mais ... en pratique?

Si WordPress n'est pas chargé, si vous essayez d'appeler les méthodes de classe ci-dessus, vous obtenez une erreur irrécupérable. Vous devez donc vous moquer des fonctions.

La méthode "manuelle"

Bien sûr, vous pouvez écrire votre bibliothèque moqueuse ou "manuellement" simuler chaque méthode. C'est possible. Je vais vous dire comment faire cela, mais ensuite je vais vous montrer une méthode plus facile.

Si WordPress n'est pas chargé pendant l'exécution des tests, cela signifie que vous pouvez redéfinir ses fonctions, par exemple add_actionou register_post_type.

Supposons que vous avez un fichier, chargé à partir de votre fichier d'amorçage, où vous avez:

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

J'ai réécrit les fonctions pour simplement ajouter un élément à un tableau global à chaque appel.

Vous devez maintenant créer (si vous n'en avez pas déjà) votre propre classe de cas de test de base étendue PHPUnit_Framework_TestCase: cela vous permet de configurer facilement vos tests.

Cela peut être quelque chose comme:

class Custom_TestCase extends \PHPUnit_Framework_TestCase {

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

}

De cette manière, avant chaque test, le compteur global est réinitialisé.

Et maintenant, votre code de test (je me réfère à la classe réécrite que j'ai postée ci-dessus):

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

}

Vous devriez noter:

  • J'ai pu appeler les deux méthodes séparément et WordPress n'est pas chargé du tout. De cette façon, si un test échoue, je sais exactement qui est le coupable.
  • Comme je l'ai dit, je teste ici que les classes appellent des fonctions WP avec les arguments attendus. Il n'est pas nécessaire de vérifier si le CPT existe réellement. Si vous testez l'existence de CPT, vous testez le comportement de WordPress, pas le comportement de votre plugin ...

Bien .. mais c'est un pita!

Oui, si vous devez vous moquer manuellement de toutes les fonctions de WordPress, c'est vraiment pénible. Un conseil général que je peux vous donner est d'utiliser le moins de fonctions possible de WP: vous n'avez pas à réécrire WordPress, mais des fonctions abstraites de WP que vous utilisez dans des classes personnalisées, afin de pouvoir les simuler et les tester facilement.

Par exemple, concernant l'exemple ci-dessus, vous pouvez écrire une classe qui enregistre des types de publication en faisant appel register_post_typeà 'init' avec des arguments donnés. Avec cette abstraction, vous devez toujours tester cette classe, mais à d'autres endroits de votre code qui enregistrent des types de publication, vous pouvez utiliser cette classe en la moquant dans des tests (en supposant que cela fonctionne).

Ce qui est génial, c’est que si vous écrivez une classe qui résume l’enregistrement CPT, vous pouvez créer un référentiel distinct et, grâce à des outils modernes comme Composer, l’ incorporer dans tous les projets pour lesquels vous en avez besoin: testez une fois, utilisez-le partout . Et si jamais vous rencontrez un bogue, vous pouvez le réparer en un seul endroit et avec un simple, composer updatetous les projets où il est utilisé sont également corrigés.

Pour la deuxième fois: écrire du code qui peut être testé isolément signifie écrire un meilleur code.

Mais tôt ou tard, j'ai besoin d'utiliser les fonctions de WP quelque part ...

Bien sûr. Vous ne devriez jamais agir parallèlement au noyau, cela n'a aucun sens. Vous pouvez écrire des classes qui encapsulent des fonctions WP, mais ces classes doivent également être testées. La méthode "manuelle" décrite ci-dessus peut être utilisée pour des tâches très simples, mais quand une classe contient beaucoup de fonctions WP, cela peut être pénible.

Heureusement, là-bas, il y a de bonnes personnes qui écrivent de bonnes choses. 10up , une des plus grandes agences WP, maintient une très bonne bibliothèque pour ceux qui veulent tester les plugins de la bonne façon. C'est WP_Mock.

Il vous permet de simuler des fonctions WP et des hooks . En supposant que vous ayez chargé dans vos tests (voir le fichier repo), le même test que j'ai écrit ci-dessus devient:

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, n'est ce pas? Cette réponse n’est pas un tutoriel pour WP_Mock, alors lisez le fichier lisez-moi pour plus d’informations, mais l’exemple ci-dessus devrait être assez clair, je pense.

De plus, vous n'avez pas besoin d'écrire vous-même add_actionou register_post_typede vous moquer , ni de maintenir des variables globales.

Et des cours WP?

WP a aussi des classes, et si WordPress n’est pas chargé lorsque vous exécutez des tests, vous devez vous en moquer.

C'est beaucoup plus facile que de se moquer de fonctions, PHPUnit a un système embarqué pour se moquer d'objets, mais je veux ici vous suggérer Mockery . C'est une bibliothèque très puissante et très facile à utiliser. De plus, c'est une dépendance de WP_Mock, donc si vous l'avez, vous avez aussi Mockery.

Mais qu'en est-il WP_UnitTestCase?

La suite de tests WordPress a été créée pour tester le noyau de WordPress , et si vous souhaitez contribuer au noyau, elle est essentielle, mais son utilisation pour les plugins ne vous permet pas de tester en vase clos.

Regardez vers le monde WP: il existe de nombreux frameworks PHP modernes et CMS, et aucun d’entre eux ne suggère de tester des plugins / modules / extensions (ou peu importe leur nom) avec du code framework.

Si vous manquez des usines, une fonctionnalité utile de la suite, vous devez savoir qu'il existe des choses incroyables là-bas.

Les pièges et les inconvénients

Il existe un cas où le flux de travail que j'ai suggéré ici manque: les tests de base de données personnalisés .

En fait, si vous utilisez des tables de WordPress et fonctions standard pour y écrire (au plus bas niveau des $wpdbméthodes) vous ne devez jamais réellement l' écriture de données ou de test si les données sont en fait dans la base de données, assurez - vous simplement que les méthodes appropriées sont appelées avec des arguments appropriés.

Cependant, vous pouvez écrire des plugins avec des tables et des fonctions personnalisées qui construisent des requêtes pour y écrire, et tester si ces requêtes fonctionnent, cela relève de votre responsabilité.

Dans ces cas, la suite de tests WordPress peut vous aider beaucoup, et le chargement de WordPress peut être nécessaire dans certains cas pour exécuter des fonctions telles que dbDelta.

(Il n'y a pas besoin de dire d'utiliser une autre base de données pour les tests, n'est-ce pas?)

Heureusement, PHPUnit vous permet d'organiser vos tests en "suites" pouvant être exécutées séparément. Vous pouvez donc écrire une suite pour des tests de base de données personnalisés dans lesquels vous chargez un environnement WordPress (ou une partie de celui-ci), laissant ainsi le reste de vos tests sans WordPress .

Assurez-vous seulement d'écrire des classes qui résument autant d'opérations de base de données que toutes les autres classes de plug-in, afin que vous puissiez tester correctement la majorité des classes sans utiliser la base de données.

Pour la troisième fois, écrire du code facilement testable de manière isolée signifie écrire un meilleur code.

gmazzap
la source
5
Putain de merde, beaucoup d'informations utiles! Merci! D'une manière ou d'une autre, j'ai réussi à rater tout le but des tests unitaires (jusqu'à présent, je pratiquais les tests PHP uniquement dans Code Dojo). J'ai aussi découvert wp_mock plus tôt dans la journée, mais je parviens à l'ignorer pour une raison quelconque. Ce qui me faisait chier, c’est que tout test, peu importe sa taille, prenait au moins deux secondes à s’exécuter (chargez WP env d’abord, exécutez la seconde de test). Merci encore d'avoir ouvert les yeux!
Ionut Staicu
4
Merci @IonutStaicu J'ai oublié de mentionner que le fait de ne pas charger WordPress rend vos tests beaucoup plus rapides
gmazzap
6
Il convient également de souligner que le cadre de tests unitaires WP Core est un outil extraordinaire pour exécuter les tests INTEGRATION. Il s’agirait de tests automatisés garantissant une bonne intégration avec WP lui-même (par exemple, il n’ya pas de collisions accidentelles entre noms de fonctions, etc.).
John P Bloch
1
@JohnPBloch +1 pour le bon point. Même si l’utilisation d’un espace de noms est suffisante pour éviter toute collision de noms de fonctions dans WordPress, où tout est global :) Mais, bien sûr, les intégrations / tests fonctionnels sont une chose. Je joue avec Behat + Mink pour le moment mais je m'entraîne toujours avec ça.
gmazzap
1
Merci pour le "tour en hélicoptère" au-dessus de la forêt UnitTest de WordPress - Je ris encore de cette photo épique ;-)
birgire