Problème classique sur un plugin d’abonnements : la logique de relance envoyait un e-mail « votre abonnement expire dans trois jours » via un événement planifié avec wp_schedule_event(). Impossible d’attendre trois jours à chaque exécution de la suite de tests pour vérifier que la relance partait bien au bon moment. La solution ne consiste pas à attendre, mais à déclencher l’événement directement et à contrôler ce que le code fait de la date courante.
WordPress ne propose pas d’API dédiée au figeage du temps comme Carbon le fait en PHP générique, mais deux leviers suffisent dans l’immense majorité des cas : appeler directement le hook cron plutôt que d’attendre son échéance, et filtrer la valeur retournée par les fonctions de date pour simuler « demain » ou « dans trois jours ».
Déclencher un événement planifié directement
Un événement cron WordPress n’est jamais qu’une action déclenchée par un hook nommé. Plutôt que d’attendre que wp_schedule_event() arrive à échéance, il suffit d’appeler do_action() sur le même hook :
class Test_Relance_Abonnement extends WP_UnitTestCase {
public function test_envoie_la_relance_a_trois_jours() {
$post_id = self::factory()->post->create( array(
'post_type' => 'abonnement',
'meta_input' => array( '_date_expiration' => '2020-05-10' ),
) );
// Déclenche directement le hook, sans attendre le cron réel.
do_action( 'mon_plugin_verifier_expirations' );
$envoyes = get_post_meta( $post_id, '_relance_envoyee', true );
$this->assertEquals( '1', $envoyes );
}
}
C’est un test d’intégration classique, mais il ne prouve qu’une chose : que le callback fonctionne à un instant T. Le vrai défi reste de simuler « le 7 mai » pour vérifier que la relance ne part ni trop tôt ni trop tard.
Figer le temps avec un filtre plutôt qu’un sleep
WordPress construit ses dates autour de current_time() et de l’option gmt_offset, mais la brique la plus simple à filtrer reste souvent le point d’entrée que votre propre code utilise pour obtenir « maintenant ». La meilleure pratique consiste justement à faire transiter ce « maintenant » par un filtre dédié dans votre plugin, précisément pour le rendre testable :
// Dans le plugin :
function mon_plugin_maintenant() {
return apply_filters( 'mon_plugin_maintenant', current_time( 'timestamp' ) );
}

// Dans le test :
public function test_ne_relance_pas_trop_tot() {
add_filter( 'mon_plugin_maintenant', function () {
return strtotime( '2020-05-06' );
} );
do_action( 'mon_plugin_verifier_expirations' );
remove_all_filters( 'mon_plugin_maintenant' );
$this->assertEmpty( get_post_meta( $this->post_id, '_relance_envoyee', true ) );
}
Cette approche évite de dépendre d’une bibliothèque de mock du temps système : elle rend le code lui-même testable en explicitant sa dépendance à la date courante. C’est un principe qui dépasse largement WP-Cron.
Vérifier qu’un événement est bien planifié
Au-delà du déclenchement, il est utile de vérifier que l’événement a été correctement enregistré dans la table des options (cron), sans quoi il ne se déclenchera jamais en production :
wp_next_scheduled( 'mon_plugin_verifier_expirations' )doit retourner un timestamp, pasfalsewp_get_scheduled_event( 'mon_plugin_verifier_expirations' )permet d’inspecter la récurrence choisie (daily,hourly, etc.)- Un test d’activation du plugin doit vérifier que
wp_schedule_event()a bien été appelé une seule fois, pour éviter les doublons d’événements à chaque réactivation
Piège fréquent : oublier de nettoyer entre les tests
La table cron est stockée dans les options, donc WP_UnitTestCase la réinitialise normalement entre chaque test grâce à la transaction annulée en fin de test. Le vrai piège survient quand le code planifie l’événement dans un hook init global : sans wp_clear_scheduled_hook() explicite en fin de test, des événements peuvent s’accumuler d’un test à l’autre et fausser des assertions sur le nombre d’événements planifiés.
Notre verdict
Tester du code dépendant du temps sous WordPress ne demande ni bibliothèque tierce ni sleep artificiel : déclencher directement le hook cron et faire transiter le « maintenant » par un filtre couvre l’essentiel des scénarios. La conception même du système cron — pourquoi WP-Cron se déclenche sur le trafic plutôt qu’en tâche système — reste un autre sujet, à part.