DEV Community

Roberto Manchado
Roberto Manchado

Posted on

Cómo testear con métodos privados en PHPUNIT

A la hora de desarrollar nuestros tests, podremos tener problemas con el ámbito de privacidad de los métodos.
Según las buenas prácticas en el desarrollo de test, estos métodos deben ser públicos, sin embargo, en algunos casos, en la implementación de una api estos métodos disponen de métodos privados y clases finales.

Aquí el ejemplo:


/**
     * @param array $data
     *
     * @return array
     */
    private function deserializeCompetitions(array $data): array
    {
        $sportArray = [];

        foreach ($data as $entry) {
            if (!empty($entry['id'])) {
                $sportArray[] = $this->deserializeCompetition($entry);
            }
        }

        return $sportArray;
    }
Enter fullscreen mode Exit fullscreen mode

Si queremos deserializar los datos para poder validar alǵun método que estemos desarrollando la única manera 'sana', para poder realizar este proceso es utilizar el propio o los propios métodos de la clase que los implementa.
El problema es que estos métodos son privados, y la clase que lo implementa es una clase final. Perfectamente lógico y además bien hecho.

En este caso, y para salir del paso, podemos usar la Api de de reflexión de PHP para manipular el ámbito de los métodos.

Esto es:

    // obtenemos un nuevo objeto con la clase ReflectionMethod .
    // los párametros son la clase que implementa el método privado, y el nombre del método privado 
    $method = new ReflectionMethod(SportsDataClient::class, 'deserializeCompetition');
    // lo configuramos para que pueda ser accesible
    $method->setAccessible(true);
    // instanciamos nuestra clase a testar
    $testClient = new SportdataClientTest();
    $contentClient = $testClient->createInstance();
    // recuperamos los datos que queremos deserializar en formato por ejemplo .json
    $optaCompetition = $this->getOptaJson('opta-competition')['competition'];
    // con _invoke, ejecutamos el método ahora accesible junto con los datos de entrada
    // los parámetros son la clase que implementa el método y el/los parámetros de entrada
    $competition = $method->invoke($contentClient, $optaCompetition);
    // creamos un Mock, con lo que podremos retornar un valor de la clase Competition
    // con los datos deserializados en el formato correspondiente.
    $sportDataclientMock
        ->expects($this->any())
        ->method('getCompetitionBySportIdAndCompetitionId')
        ->willReturn($competition);
Enter fullscreen mode Exit fullscreen mode

Y nuestro método que necesitamos :

/**
     * @param array $data
     *
     * @return Competition
     */
    private function deserializeCompetition(array $data): Competition
    {
        $sportsId = SportsDataId::build($data['id']);
        $seasons = $this->deserializeSeasons($data);

        return Competition::build(
            $sportsId,
            $data['name'] ?? '',
            $data['short_name'] ?? '',
            $data['url'] ?? '',
            $data['path'] ?? '',
            $data['active_season'] ?? '',
            $seasons
        );
    }
Enter fullscreen mode Exit fullscreen mode

De esta forma, no saltará ninguna excepción de PHP UNIT por intentar acceder a un método privado desde una clase final.

fdo: roberto manchado

Discussion (2)

Collapse
hugosantiagobecerraadan profile image
Hugo S. Becerra Adán • Edited on

No creo que sea apropiado hacer tests para métodos privados, los test deben validar el comportamiento definido desde fuera, así se prueba la clase como una unidad independiente. Se deben preparar los casos de uso con datos y las dependencias adecuadas para forzar que ejecute el método privado y este produzca el resultado esperado observable para las aserciones del test, pero siempre desde una perspectiva externa a la propia clase que se desea validar. Si tuviera que ser necesario validar un comportamiento interno de la clase de forma aislada es posible que no se esté cumpliendo el principio de responsabilidad única o que existan acoplamientos o side-effects en el comportamiento de la clase.

Collapse
robertomanchado profile image
Roberto Manchado Author

Buen apunte Hugo. Creo que no he puesto el título debidamente correcto. En mi ejemplo muestro como utilizar métodos privados ya existentes para poder utilizarlos en lugar de crear un método que deserializa. Utilizando precisamente uno que ya existe pero es privado.