7 Casos donde debes de usar Dobles de Prueba

Spread the love

Generalmente los dobles de prueba se usan para situaciones donde necesitamos simular la interacción con recursos que son difíciles de usar en una prueba.

Pero cuando comenzamos a usar dobles a veces se complica un poco saber en que casos me conviene usarlos.

Es por eso que te dejo algunas situaciones donde puedes usar dobles de prueba y también te dejo un pequeño ejemplo de cada situación.

Utiliza Mocks si el objeto real
Utiliza Mocks si el objeto real

El objeto real tiene un comportamiento no determinista.

Existen muchas situaciones en las cuales nuestro sistema bajo prueba requiere de un recurso cuyo resultado no podemos predecir de forma anticipada.

Por ejemplo cuando usamos el helper now() o la clase Str::uuid() en laravel, en realidad no podemos determinar de forma previa el valor que vamos a obtener en cada llamada durante una prueba (Test) a menos que especifiquemos su valor.

    public function test_the_filename()
    {
        $filename = "users-export-".now()->timestamp. ".xlsx";

        $this->assertEquals($filename, $this->setFileName());
    }

    public function setFileName(): string
    {
        return sprintf('users-export-%s.xlsx', now()->timestamp);
    }

Si ejecutas esta prueba vas a obtener un error, porque no tienes forma de determinar que $filename tendrá el mismo valor que $this->setFileName() en la afirmación assertEquals.

y esto se debe a que tenemos dos llamadas a la función now(), que nos darán resultados distintos en cada llamada.

Para resolver esto, necesitas buscar la forma de crear un doble que te permita tener el mismo timestamp en cada llamada.

Afortunadamente esto lo puedes hacer usando Carbon::setTestNow o $this->travelTo en Laravel 8.

veamos la solución.

    public function test_the_filename()
    {
        $this->travelTo(now());

        $filename = "users-export-".now()->timestamp. ".xlsx";

        $this->assertEquals($filename, $this->setFileName());
    }

    public function setFileName(): string
    {
        return sprintf('users-export-%s.xlsx', now()->timestamp);
    }

$this->travelTo(now()) “congela” el tiempo, así que no importa cuantas veces se use now() durante la prueba, siempre vas a obtener el mismo timestamp y con eso logras que la prueba pase.

El objeto real es difícil de configurar

Cuando queremos probar el envío de correos por lo general tenemos que crear un Mailable, una vista y configurar el servicio de correo que vamos a usar (mailtrap, smtp,etc).

El detalle es que todo esto lleva tiempo y al inicio del proyecto por lo general no esta definido que medio vas a usar para el envío de correos. Así que esto puede ser una buena oportunidad para usar un doble de prueba.

Lo bueno es que Laravel cuenta con dobles de prueba para esto y más (queues, eventos, etc), de tal forma que no requieres cambios adicionales para iniciar con pruebas de forma temprana y sin tener un servicio de smtp configurado.

   public function test_sending_of_email()
   {
       Mail::fake();

       Mail::send(new MailableTesting());
       
       //Comprueba el envío de correo
       Mail::assertSent(MailableTesting::class);
   }

El objeto real tiene un comportamiento difícil de activar

¿Cómo pruebas un fallo de red o una petición de error a un Api Rest?

Este es un ejemplo donde no podemos usar el servicio real para probar un error que proviene de un elemento externo a nuestra aplicación, en este caso un API.

Por ejemplo supongamos que quieres manejar el error que se genera al llegar al limite de peticiones a un api, obviamente no vas a lanzar peticiones al api hasta llegar al limite, necesitas crear un doble que simule que llagas a ese limite y en el código de tu aplicación manejar esa respuesta.

Este tipo de escenarios los puedes manejar fácilmente usando mocks o el fake del cliente de http que viene en las versiones recientes de Laravel.

    public function test_calling_remote_api_rest()
    {
        Http::fake(function ($request) {
            return Http::response(
                [
                    'success' => false,
                    'error' => [
                        'code' => 106,
                        'type' => "rate_limit_reached",
                        'info' => 'User has exceeded the maximum allowed rate limitation and is referred to the "Rate Limits" section of the API Documentation."',
                    ],
                ],
                200,
            );
        });

        $response = Http::get('https://apilayer.net/api/check', [
            'access_key' => 'bca1f217938af282c13e67799196a7ed',
            'email' => 'support@fakedomain.com',
            'format' => 1,
        ]);

        $this->assertFalse($response->json()['success']);
        $this->assertEquals(106, $response->json()['error']['code']);
        $this->assertEquals('rate_limit_reached', $response->json()['error']['type']);
    }

El objeto real es lento

Este escenario involucra cualquier objeto que accede a un recurso externo, como una base de datos o algún otro servicio de red.

El caso mas típico es cuando accedemos a los modelos de Laravel, estos requieren conectarse a una base de datos y es muy fácil usar un sustituto de la base de datos real por una en memoria:

    public function test_add_an_employee_to_company()
    {
        $company = Company::factory()->create();
        $employee = Employee::factory()->create();

        $company->hire($employee);

        $this->assertTrue($company->employees->contains($employee));
    }

En este ejemplo podemos cambiar los settings para usar SQLite y tendremos los mismos resultado y se ejecutara la prueba mas rapido.

<php>
        //...
        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>
        //...
    </php>

Obviamente este tipo de solución es recomendable en los casos en los que no requerimos usar features específicos de un gesto de base de datos.

También puedes hacer uso de adaptadores o de datos en memoria para probar algunos casos.

El objeto real utiliza una llamada de retorno

Existen ocasiones en las que necesitas determinar si se llama al método de una dependencia con ciertos parámetros o numero de llamadas, para estos escenarios es muy bueno usar un mock o un spy

    public function test_calculates_the_average_temperature()
    {
        //Arrange
        $service = Mockery::mock(HeatSensor::class);
        $service->shouldReceive('readTemp')
                ->times(3)->andReturn(10,12,14);
        //Act
        $temperature = new Temperature($service);

        //Assert
        $this->assertEquals(12, $temperature->average());
    }

En la prueba $service se configura para esperar a que se llame el método readTemp 3 veces; si las llamadas son menor o mayor a 3 entonces tendremos un error en la prueba.

El objeto real aún no existe

En algunas ocasiones requerimos usar clases o interfaces que todavía no existen, y el uso de Dobles de prueba es increíblemente útil en estos casos, ya que nos permiten jugar con el diseño de los colaboradores de una clase o componente de nuestro aplicación.

por ejemplo podemos usar Mockery para crear llamadas a una clase o interfaz que no existe de forma muy sencilla:

    public function test_it_post_a_tweet()
    {
        $tweet = 'this is a test';
        $twitter = \Mockery::mock(Twitter::class);
        $twitter->expects()->tweet($tweet)->andReturn(true);

        $notification = new Notification($twitter);
        $notification->send($tweet);
    }

En la prueba la clase que se esta probando (Notification) utiliza una clase que no existe, y lo interesante es que eso me permite hacer cambios en esa clase (Twitter::class) sin tener que crear código y de esta forma diseñar la api de la forma que mas me guste.

Por ejemplo imaginemos que mi ejemplo esta terminado, así que al final quedaría mi clase Notification de la siguiente forma:

class Notification
{
    private $twitter;

    public function __construct(Twitter $twitter)
    {
        $this->twitter = $twitter;
    }

    public function send($tweet)
    {
        $this->twitter->tweet($tweet);
    }
}

Twitter podría ser una clase o una interfaz, aquí la ventaja es que esta implementación se logro usando mockery sin tener una implementación real de Twitter.

¿Te gusto el articulo?

Entonces no olvides compartir y dejar tu mensaje o duda.