Essayez sans attendre l'hébergement proposé par WordPress
-15% sur le premier mois avec le code 2025PRESS15AFF

Essayer maintenant

Tester ses plugins WordPress avec PHPUnit 11 et WP_Mock : Guide complet 2026

Écrire des tests pour ses plugins WordPress, c’est souvent la bonne intention qu’on reporte à plus tard… jusqu’au jour où un bug en production nous rappelle pourquoi c’est indispensable. Avec PHPUnit 11 et WP_Mock, on dispose aujourd’hui d’un combo vraiment solide pour tester son code sans avoir besoin d’une instance WordPress complète. Dans ce guide, on va voir ensemble comment mettre en place tout l’environnement, écrire des tests concrets et les intégrer dans un pipeline CI/CD.

Mettre en place l’environnement de test

Avant d’écrire la moindre ligne de test, il faut poser des fondations solides. Et honnêtement, c’est souvent cette étape qu’on bâcle… et qu’on regrette ensuite. Voici comment configurer un environnement propre et durable pour vos tests PHPUnit 11 avec WP_Mock.

Installer PHPUnit 11 et ses dépendances

Deux prérequis sont obligatoires avant de commencer : PHP 8.1 minimum et Composer. PHPUnit 11 exploite les attributs natifs de PHP 8.x, donc pas moyen de s’en sortir avec une version antérieure.

Une fois ces prérequis en place, on installe PHPUnit 11 et WP_Mock en tant que dépendances de développement :

composer require --dev phpunit/phpunit:^11
composer require --dev 10up/wp_mock

Vérifiez bien que la version de WP_Mock installée est >= 1.0.0 — c’est la condition sine qua non pour une compatibilité correcte avec PHPUnit 11. Les versions antérieures posent des problèmes de compatibilité que vous ne voulez vraiment pas déboguer un vendredi soir.

Un point important sur PHPUnit 11 : plusieurs méthodes dépréciées dans PHPUnit 9 et 10 ont été définitivement supprimées. Par exemple, les annotations PHPDoc comme @dataProvider ou @test sont remplacées par des attributs PHP natifs :

// Ancienne syntaxe (ne fonctionne plus avec PHPUnit 11)
/** @test */
public function it_should_do_something() {}

// Nouvelle syntaxe avec attributs PHP 8.x
#[\PHPUnit\Framework\Attributes\Test]
public function it_should_do_something() {}

C’est un changement de taille si vous migrez depuis une version ancienne. Prévoyez du temps pour mettre à jour vos tests existants.

Configurer WP_Mock dans votre projet

La configuration se fait principalement via un fichier phpunit.xml (ou phpunit.xml.dist si vous versionnez le projet en équipe) à la racine du plugin. Voici un exemple fonctionnel :

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    bootstrap="tests/bootstrap.php"
    colors="true"
    stopOnFailure="false"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
>
    <testsuites>
        <testsuite name="Unit Tests">
            <directory suffix="Test.php">tests/Unit</directory>
        </testsuite>
    </testsuites>
</phpunit>

La balise <bootstrap> pointe vers le fichier d’initialisation de WP_Mock. Ce fichier tests/bootstrap.php ressemble à ça :

<?php

require_once dirname( __DIR__ ) . '/vendor/autoload.php';

WP_Mock::bootstrap();

Simple, mais essentiel. C’est lui qui charge l’autoloader Composer et initialise WP_Mock pour tous vos tests.

Structurer les dossiers de tests dans votre plugin

Une bonne organisation dès le départ vous évitera bien des maux de tête. La structure recommandée est la suivante :

mon-plugin/
├── src/
│   └── MyClass.php
├── tests/
│   ├── Unit/
│   │   └── MyClassTest.php
│   └── bootstrap.php
├── composer.json
└── phpunit.xml

Quelques règles à respecter :

  • tests/Unit/ : tous vos fichiers de tests unitaires, suffixés par Test.php
  • tests/bootstrap.php : initialisation de l’environnement (WP_Mock, autoloader)
  • Les tests doivent refléter la structure de src/ pour s’y retrouver facilement

Cette structure est celle qu’on retrouve dans la plupart des plugins WordPress maintenus sérieusement. Et c’est aussi celle que PHPUnit attend par défaut avec la configuration XML décrite plus haut. Donc autant partir du bon pied !

Écrire vos premiers tests unitaires avec WP_Mock

Maintenant qu’on a notre environnement en place, il est temps de passer aux choses sérieuses. Écrire des tests pour un plugin WordPress, c’est un peu particulier : WordPress repose massivement sur des fonctions globales (get_option(), apply_filters(), add_action()…) qui n’existent tout simplement pas quand PHPUnit tourne en dehors de WordPress. C’est exactement là que WP_Mock entre en scène.

Comprendre le principe du mock pour WordPress

Un « mock », c’est un faux objet (ou une fausse fonction) qui remplace la vraie pendant les tests. L’idée est simple : on ne veut pas charger tout WordPress juste pour tester une petite fonction de notre plugin. Ce serait lent, fragile et franchement inutile.

WP_Mock permet de déclarer à l’avance : « quand mon code appelle get_option('ma_cle'), renvoie cette valeur ». On contrôle ainsi l’environnement de test à 100%, sans dépendance à la base de données ou au cœur de WordPress. C’est ce qu’on appelle un test unitaire : on isole une unité de code et on vérifie son comportement de façon déterministe.

Tester une fonction qui appelle des hooks WordPress

Prenons un exemple concret. Voici une fonction dans notre plugin qui récupère un réglage et le filtre :

// includes/functions.php
function myplugin_get_title() {
    $title = get_option( 'myplugin_title', 'Titre par défaut' );
    return apply_filters( 'myplugin_title', $title );
}

Et voici le test correspondant avec WP_Mock :

// tests/Unit/FunctionsTest.php
<?php

use PHPUnit\Framework\TestCase;

class FunctionsTest extends TestCase {

    public function setUp(): void {
        parent::setUp();
        WP_Mock::setUp();
    }

    public function tearDown(): void {
        WP_Mock::tearDown();
        parent::tearDown();
    }

    public function test_myplugin_get_title_returns_filtered_option(): void {
        WP_Mock::userFunction( 'get_option', [
            'args'   => [ 'myplugin_title', 'Titre par défaut' ],
            'return' => 'Mon super titre',
            'times'  => 1,
        ] );

        WP_Mock::onFilter( 'myplugin_title' )
            ->with( 'Mon super titre' )
            ->reply( 'Mon super titre filtré' );

        $result = myplugin_get_title();

        $this->assertSame( 'Mon super titre filtré', $result );
    }
}

Quelques points importants ici : WP_Mock::userFunction() simule get_option() avec les arguments exacts attendus. Le paramètre times vérifie que la fonction est appelée exactement une fois (pratique pour détecter les appels en double). Et WP_Mock::onFilter() simule le comportement d’apply_filters() de façon fluide.

Tester une classe de plugin avec des fonctions WordPress mockées

En pratique, on travaille souvent avec des classes. Voici un exemple avec une classe Settings :

// src/Settings.php
<?php

namespace MyPlugin;

class Settings {

    public function get_api_key(): string {
        $key = get_option( 'myplugin_api_key', '' );
        if ( empty( $key ) ) {
            return '';
        }
        return sanitize_text_field( $key );
    }

    public function is_enabled(): bool {
        return (bool) get_option( 'myplugin_enabled', false );
    }
}

Le test de cette classe ressemble à ça :

// tests/Unit/SettingsTest.php
<?php

use MyPlugin\Settings;
use PHPUnit\Framework\TestCase;

class SettingsTest extends TestCase {

    private Settings $settings;

    public function setUp(): void {
        parent::setUp();
        WP_Mock::setUp();
        $this->settings = new Settings();
    }

    public function tearDown(): void {
        WP_Mock::tearDown();
        parent::tearDown();
    }

    public function test_get_api_key_returns_sanitized_value(): void {
        WP_Mock::userFunction( 'get_option', [
            'args'   => [ 'myplugin_api_key', '' ],
            'return' => '  ma-clé-api  ',
        ] );

        WP_Mock::userFunction( 'sanitize_text_field', [
            'args'   => [ '  ma-clé-api  ' ],
            'return' => 'ma-clé-api',
        ] );

        $this->assertSame( 'ma-clé-api', $this->settings->get_api_key() );
    }

    public function test_get_api_key_returns_empty_string_when_no_option(): void {
        WP_Mock::userFunction( 'get_option', [
            'args'   => [ 'myplugin_api_key', '' ],
            'return' => '',
        ] );

        $this->assertSame( '', $this->settings->get_api_key() );
    }

    public function test_is_enabled_returns_false_by_default(): void {
        WP_Mock::userFunction( 'get_option', [
            'args'   => [ 'myplugin_enabled', false ],
            'return' => false,
        ] );

        $this->assertFalse( $this->settings->is_enabled() );
    }
}

Notez que WP_Mock::setUp() et WP_Mock::tearDown() sont appelés dans chaque test (via setUp() et tearDown() de PHPUnit). C’est indispensable : WP_Mock vérifie à la fin de chaque test que toutes les fonctions mockées ont bien été appelées comme prévu. Si ce n’est pas le cas, il lève une erreur. Un filet de sécurité très utile.

Gérer les assertions et les cas limites

PHPUnit 11 propose un ensemble d’assertions bien fourni. Pour les tests de plugins WordPress, on utilise surtout :

  • assertSame() : vérifie la valeur et le type (préférez-le à assertEquals() pour éviter les surprises)
  • assertEquals() : vérifie la valeur avec une comparaison lâche
  • assertNull() / assertNotNull() : pratique pour les retours optionnels
  • assertTrue() / assertFalse() : pour les booléens
  • assertInstanceOf() : pour vérifier le type d’un objet retourné

Et pour les cas limites, pensez aux exceptions. Si votre méthode est censée lever une exception dans certains cas, testez-le explicitement :

public function test_throws_exception_on_invalid_input(): void {
    $this->expectException( \InvalidArgumentException::class );
    $this->expectExceptionMessage( 'La clé API ne peut pas être vide.' );

    $this->settings->save_api_key( '' );
}

Bon, une dernière chose à garder en tête : ne cherchez pas à tout mocker. Si une méthode est purement PHP (sans appel WordPress), testez-la directement sans WP_Mock. Moins de mocks, c’est souvent des tests plus clairs et plus fiables sur le long terme.

Lancer les tests et interpréter les résultats

On a installé PHPUnit, configuré WP_Mock, écrit nos premiers tests… il ne reste plus qu’à appuyer sur le bouton. La commande de base, c’est simplement :

./vendor/bin/phpunit

Et là, PHPUnit parcourt tous vos fichiers de test selon la configuration phpunit.xml définie plus tôt. Mais quelques options méritent vraiment d’être connues :

  • --filter nomDuTest : lance uniquement un test ou une classe spécifique (pratique quand on débogue un cas précis sans vouloir tout relancer)
  • --testdox : affiche les résultats sous forme de phrases lisibles — beaucoup plus agréable que les points bruts
  • --coverage-text : affiche un résumé de couverture de code directement dans le terminal (nécessite Xdebug ou PCOV)

La lecture du rapport, c’est assez intuitif une fois qu’on a compris le code : un point . signifie que le test est passé, F indique un échec (failure, c’est-à-dire qu’une assertion n’est pas vérifiée), et E signale une erreur PHP inattendue. En résumé : on vise un maximum de points, et on corrige les F et E en priorité.

Pour générer un rapport de couverture HTML (bien plus lisible pour analyser les zones non testées), on utilise :

XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html coverage/

Le dossier coverage/ contiendra un rapport complet navigable dans votre navigateur. (Pensez à l’ajouter dans votre .gitignore, ça n’a pas vocation à être versionné.)

Côté CI/CD, l’intégration dans GitHub Actions est vraiment simple. Un fichier .github/workflows/tests.yml minimal ressemble à ça :

name: Tests PHPUnit

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          coverage: xdebug
      - name: Install dependencies
        run: composer install --prefer-dist --no-progress
      - name: Run tests
        run: ./vendor/bin/phpunit --coverage-text

C’est tout. À chaque push, vos tests tournent automatiquement — et vous êtes alerté immédiatement si quelque chose casse.

Enfin, une question revient souvent : quel taux de couverture viser ? Honnêtement, 60 à 70 % pour un plugin WordPress, c’est déjà très bien. Ne vous fixez pas un objectif de 100 % à tout prix : certaines parties du code (hooks d’activation, interactions directes avec la base de données) sont difficiles à couvrir sans une intégration complète. Mieux vaut des tests pertinents à 65 % qu’une couverture artificielle à 90 % avec des tests qui ne testent rien de réel.