Cómo escribir pruebas unitarias para valores aleatorios en php

photo of two red dices
Photo by Jonathan Petersson on Pexels.com

Te decidiste: llegó el momento de implementar phpUnit en proyecto. Emocionante, ¿no? Por fin vas a poder considerarte un desarrollador profesional.

Ya instalaste las herramientas, te leiste unos cuantos tutoriales… todo listo.

Los primeros tests no fueron tan complicados. Llegar al esperado verde costó un poco al comienzo pero lo sacaste.

Y justo cuando la cosa se ponía interesante, te encontrás con:

<?php

class MyClass
{
    public function testableMethod(int $param) : int
    {
        if (rand(1, 100) < 50) {
            return $param * 2;       
        } else {
            return $param * 3;
        }
    }
}

¿Y ahora? ¿Cómo verificar que el método hace lo que tiene que hacer si el resultado final depende de algo que no podés controlar?

Está claro que TDD no funcionará para este caso… mejor dejar todo esto atrás y seguir trabajando como hasta ahora… al fin y al cabo, tan mal no te ha ido, ¿cierto?

Si estás pensando esto, te propongo que sigas leyendo. Hay una salida a este dilema.

La solución se basa en una característica muy popular, aunque muchas veces no muy comprendida, de la Programación Orientada a Objetos: la herencia.

¿Cómo? ¿Qué tiene que ver la herencia en todo esto? Veámoslo.

Preparando el código para las pruebas unitarias

Tu primera aproximación a un test probablemente se verá similar a:

<?php

use PHPUnit\Framework\TestCase;

use MyClass;

class MyClassTest extends TestCase
{
    public function testTestableMethod()
    {
        $sut = new MyClass();
        $this->assertEquals(..., $sut->testableMethod(5));
    }
}

El problema es, precisamente, con qué rellenar los .... Pues bien, ahí es donde viene la magia.

¿Qué tal si en lugar de testar directamente MyClass usaras una instancia de algo muy parecido a MyClass? Por ejemplo, una clase derivada de MyClass.

Vamos por partes mejor.

Comencemos por hacer a testableMethod un poco más test-friendly.

Algo muy sencillo (y 100% seguro) de hacer es aislar la parte del método testableMethod que está impidiendo hacer el test. En este caso, el problema es la obtención del número aleatorio (rand(1, 100)), así que, el primer paso sería hacer una nueva versión del método testableMethod para que se vea así:

public function testableMethod(int $param) : int
{
    if ($this->getRandomNumber() < 50) {
        return $param * 2;
    } else {
        return $param * 3;
    }
}

Y el nuevo método getRandomNumber se vería así:

protected function getRandomNumber(): int
{
    return rand(1, 100);
}

Es claro que a nivel funcional no ha cambiado nada. Sin embargo, como veremos pronto, esta sutil diferencia es crucial para el desarrollo de la prueba unitaria.

Un punto sumamente interesante es que este cambio está libre de riesgo en tanto que puede ser realizado en forma automática por un IDE.

Continuemos entonces con el test.

¿Qué tal si creamos una nueva clase TestableMyClass que tenga exactamente el mismo comportamiento que MyClass, salvo por la forma de responder a getRandomNumber?

class TestableMyClass extends MyClass
{
    private int $randomNumber;

    /**
     * @param int $randomNumber
     */
    public function __construct(int $randomNumber) 
    {
        $this->randomNumber = $randomNumber;
    }

    /**
     * @return int
     */
    protected function getRandomNumber(): int
    {
        return $this->randomNumber;
    }
}

De esta forma tenemos una nueva implementación de MyClass que, en lugar de retornar números aleatorios, retornará un valor que nosotros controlamos y, de esa forma, podremos realizar el test que buscamos:

class MyClassTest extends TestCase
{
    /**
     * @dataProvider randomNumberProvider
     * @param int $baseNumber
     * @param int $multiplier
     * @param int $randomNumber
     * @return void
     */
    public function testTestableMethod(int $baseNumber, int $multiplier, int $randomNumber): void
    {
        $sut = new TestableMyClass($randomNumber);

        $this->assertEquals($baseNumber * $multiplier, $sut->testableMethod($baseNumber));
    }

    /**
     * @return int[][]
     */
    public function randomNumberProvider(): array
    {
        return [
            [ 1, 2, 2 ],
            [ 20, 2, 40 ],
            [ 51, 3, 153 ],
            [ 60, 3, 180 ],
        ];
    }
}

Y voilà! Tenemos un test que garantiza la correctitud del método.

De hecho, para hacerlo todavía más interesante podríamos usar una clase que genere números aleatorios e inyectarla como colaborador de MyClass pero bueno… tema para otro post.

mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a afinar sus habilidades técnicas y avanzar en sus carreras

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.