# « Call to a member function on null » : mock Sentry mal initialisé

> Une doublure de test incomplète pour le SDK Sentry casse dès qu'une méthode inattendue est appelée. Voici comment la diagnostiquer et la corriger durablement.

- Auteur : Clément Hadrot
- Publié le : 2025-02-11
- Mis à jour le : 2025-02-11
- Catégorie : Tests
- URL : https://wpmoderne.dev.wordpress-developpement.fr/tests/mock-sentry-call-member-function-null/

## L’essentiel

- Un mock trop strict imite mal l'API réelle
- Préférer un test double fidèle à l'interface
- Vérifier les appels réellement effectués

« Call to a member function captureException() on null. » Ce message tombe en pleine exécution d'une suite PHPUnit qui, la veille encore, passait sans accroc. La cause : un test qui simule le SDK Sentry pour éviter d'envoyer de vraies erreurs pendant la CI, mais dont la doublure ne couvre pas toutes les méthodes que le code de production appelle réellement.

Ce genre de panne est trompeur, car elle ne révèle pas un bug applicatif : elle révèle un mock incomplet. Le code fonctionne très bien en production, où le vrai client Sentry existe. C'est uniquement dans l'environnement de test, où une version simplifiée du client a été substituée, que la fissure apparaît. Comprendre ce mécanisme évite de perdre une demi-journée à chercher un problème qui n'existe pas.

## Symptôme

Le scénario typique : un service applicatif capture une exception via `Sentry\captureException()` ou via une instance de client injectée. En test, ce client est remplacé par un double construit à la main, qui ne définit que la méthode utilisée dans le chemin heureux du test — par exemple `captureMessage()` — en laissant les autres méthodes absentes ou renvoyant `null` par défaut.

Quand le code appelle ensuite `configureScope()` puis tente d'invoquer une méthode sur la valeur retournée, PHP lève une `Error` fatale : la valeur retournée est `null`, pas un objet `Scope`. Le message d'erreur pointe vers la ligne du code métier, ce qui pousse souvent à chercher le bug au mauvais endroit.

> L'essentiel à retenir : Un mock trop strict imite mal l'API réelle ; Préférer un test double fidèle à l'interface ; Vérifier les appels réellement effectués

## Diagnostic

La première étape consiste à isoler l'appel fautif avec un test minimal qui reproduit exactement l'enchaînement d'appels du code de production, sans passer par toute la suite. Cela confirme que le problème vient du double, pas d'une régression métier :

```
public function test_capture_avec_scope_personnalise(): void
{
    $client = $this->createMock(ClientInterface::class);
    $hub = new Hub($client);

    // Reproduit l'appel réel du service
    $hub->withScope(function (Scope $scope) {
        $scope->setTag('module', 'facturation');
    });

    $this->assertTrue(true); // le test échoue avant d'arriver ici
}
```

Le mock généré par `createMock()` sur une interface renvoie `null` pour toute méthode non explicitement configurée avec `willReturn()`. Si `withScope()` attend un callable recevant un objet `Scope` réel et que le mock ne simule pas ce comportement, l'erreur éclate à l'intérieur de la closure du test lui-même, pas dans le service.

- Repérer la méthode exacte citée dans la trace de la pile d'appels
- Vérifier si cette méthode est définie sur le mock ou héritée par défaut
- Comparer la signature attendue par le SDK avec celle simulée

## Correctif

La solution la plus robuste consiste à ne pas mocker le `Hub` Sentry directement, mais à passer par le `HubInterface` avec un double qui simule fidèlement le comportement de `withScope()`, y compris l'exécution de la closure passée en argument :

```
$hub = $this->createMock(HubInterface::class);
$hub->method('withScope')
    ->willReturnCallback(function (callable $callback) {
        $scope = new Scope();
        return $callback($scope);
    });
```

Cette approche exécute réellement la closure fournie par le code testé, avec un objet `Scope` authentique plutôt qu'un mock partiel. Le test valide alors le comportement réel du service, sans dépendre de l'exhaustivité manuelle du double.

Une alternative plus légère, quand le projet n'a pas besoin de vérifier les tags envoyés à Sentry, consiste à injecter une implémentation nulle explicite plutôt qu'un mock : une classe `NullHub` qui implémente chaque méthode de l'interface avec un corps vide ou un retour cohérent. Le code appelant ne rencontre alors jamais de `null` inattendu.

### Vérifier les appels effectués

Pour s'assurer que l'exception est bien transmise à Sentry, une assertion sur le nombre d'invocations reste utile :

```
$hub->expects($this->once())
    ->method('captureException')
    ->with($this->isInstanceOf(RuntimeException::class));
```

> Un test qui simule une dépendance externe doit couvrir toute l'interface utilisée par le code réel, pas seulement le chemin nominal du scénario en cours d'écriture.

## Prévention

Pour éviter que ce type de panne ne resurgisse à chaque évolution du code d'instrumentation, quelques réflexes limitent le risque :

- Centraliser la création du double Sentry dans une fabrique de test unique, réutilisée par tous les tests concernés
- Ajouter un test dédié qui vérifie explicitement le comportement de `withScope()`, indépendamment des autres scénarios
- Préférer `PHPStan` avec le niveau strict sur les interfaces tierces pour repérer les appels sur des types nullable

Un audit rapide de tous les points d'utilisation du SDK, via une recherche de `Sentry\configureScope` et `withScope` dans le code source, permet de vérifier que la fabrique de mock couvre bien chaque usage avant qu'un nouveau développeur ne découvre la panne en pleine CI.

## En résumé

Une erreur « Call to a member function on null » dans un test qui touche à l'observabilité indique presque toujours un double incomplet plutôt qu'un bug de production. Traiter la doublure de test comme un contrat à honorer intégralement — pas seulement pour le chemin heureux — évite ces sueurs froides en fin de sprint, et rend la suite de tests plus fiable sur le long terme.
