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 , init
hook devrait se déclencher, mais ... ce n’est pas le cas; De plus, le code fonctionne dans WordPress.
De mon point de vue, 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!
Lors du développement d'un plugin, le meilleur moyen de le tester est sans chargement de 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é dans isolation : lorsque vous testez une classe, il vous suffit de tester cette classe spécifique, en supposant que tout le code fonctionne parfaitement.
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.
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 ?
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:
init
en faitappelle add_action
en lui passant les arguments appropriésregister_post_type
en faitappelle la fonction register_post_type
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é must existe: s'il n'existe pas, c'est 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 la signification de unit test.
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.
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_action
ou 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.
Maintenant, vous devriez créer (si vous n'en avez pas déjà) votre propre classe de cas de test de base étendue PHPUnit_Framework_TestCase
: qui 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 au réécritclasse que j’ai posté 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:
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 possible de fonctions WP: vous n’avez pas besoin de réécrireWordPress, mais de résuméWP fonctions _ utiliser dans les classes personnalisées, de sorte qu'elles puissent être simulées et facilement testées.
Par exemple. En ce qui concerne l'exemple ci-dessus, vous pouvez écrire une classe qui enregistre des types de publication en appelant register_post_type
sur '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 séparé pour elle, et grâce à des outils modernes tels que Composer , intégrez-le dans tous les projets où vous en avez besoin: test une fois, utilisez partout . Et si jamais vous rencontrez un bogue, vous pouvez le réparer à un endroit et avec un simple composer update
, tous 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.
Bien sûr. Vous ne devez jamais agir en parallèleau cœur, cela n’a aucun sens. Vous pouvez écrire des classes qui encapsulent les fonctions WP, mais ces classes doivent également être testées. La méthode "manuelle" décrite ci-dessus peut être utilisé pour des tâches très simples, mais quand une classe contient beaucoup de WP fonctions, cela peut être pénible.
Heureusement, là-bas, il y a de bonnes personnes qui écrivent de bonnes choses.10up, l'une des plus grandes agences WP, gère une très grande bibliothèque pour les personnes souhaitant tester des plugins de la bonne manière. C'est WP_Mock
.
Il vous permet de simuler WP fonctionne avec un hook . En supposant que vous ayez chargé dans vos tests (voir le fichier repo), le 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
, lisez donc 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 un add_action
ou d'un register_post_type
simulé, ni de maintenir des variables globales.
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 souhaite 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.
WP_UnitTestCase
?La suite de tests WordPress a été créée pour tester WordPress core , et si vous souhaitez contribuer à core, elle est essentielle, mais son utilisation pour les plugins ne vous permet de tester que de manière isolée.
Placez votre oeil sur le monde WP: il existe de nombreux frameworks et CMS PHP modernes et aucun d’entre eux ne suggère de tester les plugins/modules/extensions (ou peu importe leur nom) en utilisant code cadre.
Si vous manquez des usines, une fonctionnalité utile de la suite, vous devez savoir qu'il y a des choses impressionnantes là-bas.
Il existe un cas où le flux de travail que j'ai suggéré ici manque: base de données personnalisée test .
En fait, si vous utilisez des tables et des fonctions WordPress standard pour y écrire (au plus bas niveau des méthodes $wpdb
), vous n'avez jamais besoin de en faitécrire des données ou tester si les données sont en faitdans la base de données), assurez-vous simplement que les méthodes appropriées sont appelées avec les 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 ainsi é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) en laissant le reste de vos tests WordPress- gratuit.
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.