Sur un projet de gestion de formations professionnelles, la fonction qui calcule le tarif final d’une inscription — remises, quotas, majoration tardive — appelait directement get_post_meta(), wp_get_object_terms() et update_post_meta() en une seule fonction de 90 lignes. La tester correctement obligeait à créer un article WordPress complet en base pour chaque scénario de tarification, soit plus d’une seconde par test rien que pour l’initialisation des fixtures. Avec une trentaine de scénarios de tarification à couvrir, la suite dédiée à cette seule fonction prenait plus d’une minute.
Le problème ne venait pas de PHPUnit ni de WP_UnitTestCase, mais de la conception : le calcul, une pure fonction mathématique, était inutilement couplé à la persistance WordPress.
Repérer le mélange des responsabilités
La fonction originale ressemblait à ceci — un calcul pur noyé au milieu de lectures et d’écritures WordPress :
function calculer_tarif_inscription(int $inscription_id): float {
$tarif_base = (float) get_post_meta($inscription_id, 'tarif_base', true);
$quota_restant = (int) get_post_meta($inscription_id, 'quota_restant', true);
$categories = wp_get_object_terms($inscription_id, 'categorie_formation');
$remise = 0.0;
if ($quota_restant < 5) {
$remise = 0.10;
}
foreach ($categories as $categorie) {
if ($categorie->slug === 'formation-longue') {
$remise += 0.05;
}
}
$tarif_final = $tarif_base * (1 - $remise);
update_post_meta($inscription_id, 'tarif_calcule', $tarif_final);
return $tarif_final;
}
Extraire le calcul dans une classe pure
Le principe : une classe qui ne connaît rien de WordPress, ne reçoit que des types PHP natifs en entrée, et ne produit qu’un type natif en sortie. Aucun appel à une fonction préfixée wp_ ou get_post_meta n’y figure :
final class Calculateur_Tarif_Inscription {
public function calculer(float $tarif_base, int $quota_restant, array $slugs_categories): float {
$remise = 0.0;
if ($quota_restant < 5) {
$remise += 0.10;
}
if (in_array('formation-longue', $slugs_categories, true)) {
$remise += 0.05;
}
return $tarif_base * (1 - $remise);
}
}

La fonction WordPress devient une simple façade
La fonction originale garde son rôle d'orchestration — lire les données, appeler le calcul, persister le résultat — mais délègue tout le raisonnement métier à la classe pure :
function calculer_tarif_inscription(int $inscription_id): float {
$tarif_base = (float) get_post_meta($inscription_id, 'tarif_base', true);
$quota_restant = (int) get_post_meta($inscription_id, 'quota_restant', true);
$slugs = wp_list_pluck(wp_get_object_terms($inscription_id, 'categorie_formation'), 'slug');
$tarif_final = (new Calculateur_Tarif_Inscription())->calculer($tarif_base, $quota_restant, $slugs);
update_post_meta($inscription_id, 'tarif_calcule', $tarif_final);
return $tarif_final;
}
Le gain immédiat sur les tests
Les trente scénarios de tarification se testent désormais sans base de données, sans WP_UnitTestCase, avec une simple classe TestCase de PHPUnit standard :
final class Test_Calculateur_Tarif_Inscription extends \PHPUnit\Framework\TestCase {
public function test_remise_quota_faible(): void {
$calculateur = new Calculateur_Tarif_Inscription();
$resultat = $calculateur->calculer(300.0, 3, []);
$this->assertEquals(270.0, $resultat);
}
public function test_cumul_remise_quota_et_formation_longue(): void {
$calculateur = new Calculateur_Tarif_Inscription();
$resultat = $calculateur->calculer(300.0, 3, ['formation-longue']);
$this->assertEquals(255.0, $resultat);
}
}
Ces tests s'exécutent en quelques millisecondes chacun, sans transaction de base de données à ouvrir ni à annuler — un gain qui devient significatif dès que la suite dépasse quelques dizaines de scénarios.
Arborescence type d'un projet qui applique ce pattern
src/
├── Domain/
│ ├── Calculateur_Tarif_Inscription.php (logique pure, testée en millisecondes)
│ └── Regles_Quota.php
├── WordPress/
│ ├── inscriptions-hooks.php (façade, appelle le Domain)
│ └── inscriptions-rest.php
tests/
├── Unit/
│ └── Test_Calculateur_Tarif_Inscription.php (PHPUnit\Framework\TestCase)
└── Integration/
└── Test_Inscription_Complete.php (WP_UnitTestCase)
Une classe qui ne peut pas être testée sans une base de données n'est presque jamais une classe qui a vraiment besoin d'une base de données — c'est souvent une classe qui n'a simplement jamais été séparée de sa persistance.
Limites de ce pattern
Ce découplage ne concerne que la logique de calcul et de décision, jamais la persistance elle-même ni les hooks WordPress, dont le comportement reste vérifié par des tests d'intégration classiques avec base de données — un sujet traité séparément. Extraire une classe pure pour un calcul trivial d'une seule ligne n'apporte souvent aucun bénéfice ; l'effort se justifie à partir du moment où la logique comporte plusieurs branches conditionnelles qui méritent une couverture de test fine.
En résumé
Séparer la logique métier pure de son intégration WordPress n'est pas un exercice architectural abstrait : c'est ce qui a permis, sur ce projet de formations professionnelles, de faire passer une suite de tests de tarification de plus d'une minute à moins d'une seconde, tout en rendant chaque scénario de remise plus facile à lire et à faire évoluer.