PHPUnit : mocker une fonction native (simplement)

test unitaire

Lors de la rédaction de tests unitaires, une pratique courante est la réalisation de mocks / stubs afin de pouvoir répondre au principe de tests en isolation.

Cependant, si c’est relativement facile de réaliser des mocks pour un objet interne, il y a peu de littérature sur la bonne manière (avec phpunit en tous cas) pour mocker une fonction native de PHP.

Le bon

Exemple de classe facilement mockable:

<?php
namespace CCMBenchmarkMyservice;
class MyClass
{
    protected $db;
    public function __construct(Database $db){
        $this->db = $db;
    }
 
    public function myFunction()
    {
        $roles = $this->db->fetch('roles');
        $return = array();
 
        /**
        * Some magical logic, takes $roles and build a new array
        */
 
        return $array;
    }
}

 

Exemple de test pour myFunction :

<?php
namespace CCMBenchmarkTestsMyservice;
class MyClassTest extends PHPUnit_Framework_TestCase
{
    public function myFunctionShouldReturnArrayTest()
    {
        $mock =  $this->getMockBuilder('CCMBenchmarkMyServiceDatabase')
                 ->getMock();
        $mock->method('fetch')
             ->willReturn(['ADMIN', 'USER']);
        $class = new MyClass($mock);
        $this->assertArray($class->myFunction());
    }
}

Mais imaginons désormais que ma classe doive aller lire un fichier. Pas un gros boulot mais juste faire une petite lecture de fichier.

La brute

<?php
namespace CCMBenchmarkMyservice;
class MyClass
{   
    public function myFunction()
    {
        $roles = file_get_contents('roles.csv');
        $return = array();
 
        /**
        * Some magical logic, takes $roles and build a new array
        */
 
        return $array;
    }
}

La plupart des solutions que l’on retrouve évoque la nécessité de déporter la lecture du fichier roles.csv dans un nouvel objet. Mouais, pourquoi pas. Mais c’est un peu lourd tout de même ? Surtout qu’aller modifier l’application pour simplement pouvoir la tester correctement (et ajouter de l’abstraction là où ça n’est pas nécessaire) c’est tout de même overkill. Une autre solution qu’on trouve est le passage par une appli à part pour abstraire le filesystem (vfsstream). Tout ça pour un test ?

Le truand !

KeepItSimpleStupid-1920x1200

La solution la plus simple trouve dans les namespaces !

En effet, lors d’un appel de fonction, PHP va commencer par chercher une fonction dans le namespace courant. S’il ne la trouve pas, alors il basculera vers le namespace global. Cette solution parfaitement viable, est peu utilisée au quotidien à cause, notamment, de l’absence d’autoload sur les fonctions. Voici comment la mettre en oeuvre sur notre cas.

<?php
namespace CCMBenchmarkMyservice {
    function file_get_contents($myFile)
    {
         return 'ADMIN;USER' . "n";
    }
}
namespace CCMBenchmarkTestsMyservice {
    class MyClassTest extends PHPUnit_Framework_TestCase
    {
        public function myFunctionShouldReturnArrayTest()
        {
            $class = new MyClass();
            $this->assertArray($class->myFunction());
        }
    }
}

C’est tout de même pas mal, non ?

Et pour ceux que ça intéresse, Atoum a l’avantage et l’intérêt de proposer une API pour ce genre de choses. Rendant de cette manière le mock de méthodes natives aussi simple que le mock de n’importe quelle classe.

Vous aimerez aussi...

3 réponses

  1. Gaétan dit :

    Question ‘idiote’ : comment maitriser les params rentrés à la fonction pour permettre tout de même l’utilisation de la fonction native ?

    J’ai pensé à un switch / case sur $myFile pour avoir une sortie différente suivant les cas qu’on veut tester, avec en default un appel à file_get_contents. Problème : ne rentre-t-on pas dans une boucle folle ? Il y aurait quand même moyen de faire appel au file_get_contents natif ?

  2. Xavier dit :

    @Gaétan : pour les paramètres, c’est à toi de respecter la signature de la fonction originale et, donc, de faire ce que tu veux des paramètres pour choisir si tu dois faire appel à la fonction originale.

    Pour appeler la fonction originale, il suffit de préfixer son appel avec « \ », pour faire un appel dans le namespace global. Ici ça donnerait un truc du genre :

     function file_get_contents($myFile)
        {
             if ($myFile === 'roles.csv') {
                 return 'ADMIN;USER' . "\n";
             }
             return \file_get_contents($myFile);
        }
    

  3. Gaétan dit :

    nickel ! ça me va, et ça permet de vraiment maîtriser les données qu’on veut recevoir du file_get_contents tout en laissant la possibilité d’utiliser la fonction native

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *