Cómo comprobar la validación de datos en Laravel (sin pruebas manuales)

Spread the love

¿Cómo pruebas esta validación de datos en tu código?

    protected function validateLogin(Request $request)
    {
        $request->validate([
            $this->username() => 'required|string',
            'password' => 'required|string',
        ]);
    }

    public function login(Request $request)
    {
        $this->validateLogin($request);

        //more code
    }

Muy probablemente para probar esto uses el navegador, y pases datos inválidos mediante el formulario de login para ver si obtienes el resultado esperado; que es ver el mensaje de error de que una validación no paso.

Este proceso al ser repetitivo se vuelve muy molesto porque tienes que hacer todo de nuevo mientras ajustas las validaciones a los casos que tienes que probar.

¿Existe otra forma de hacer esto?

claro que sí, usar pruebas de integración para evaluar las validaciones que están en el controlador-

Pruebas de integración

Afortunadamente Laravel cuenta con todo lo necesario en este tema, puedes hacer pruebas de unidad, integración y de aceptación.

Para el caso que estoy planteando vamos a usar pruebas de integración, que nos van a permitir simular la acción de enviar los datos al login y verificar que las validaciones funcionan como esperamos.

Crear una prueba de integración

Para crea una prueba de integración es muy sencillo solo tenemos que ejecutar el siguiente comando.

λ php artisan make:test LoginControllerTest

Después de ejecutar el comando: abre el archivo tests/Feature/LoginControllerTest, este archivo solo tiene un método el cual vamos a quitar para comenzar a trabajar.

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class LoginControllersTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testExample()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

Muy bien ya que borraste el método de prueba: lo siguiente es analizar como vamos a probar las validaciones y como comente hace un momento, esto lo vamos a hacer simulando un poco lo que haces con el navegador.

Analizando el problema

Mirando el formulario de login en resource/views/auth/login.blade.php puedes

<form method="POST" action="{{ route('login') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">

                            </div>
                        </div>
                    </form>

observa que el formulario que envía los datos (email y password) mediante el método post a la ruta route(‘login’) .

Ahora si observamos las parte de la prueba puedes ver que en ambos campos solo se valida que el campo sea requerido y que sea un valor de tipo string.

vamos a colocar las pruebas en una tabla

Input / PruebaRequiredString
emailnull26.7
passwordnull26.7

Con esta tabla puedes darte cuenta que para probar tan solo dos valores necesitas por lo menos dos casos para cada input.

Esto quiere decir, que por ejemplo para probar el campo email para los casos de campo requerido y de string tendríamos dos pruebas:

/**
     * @test
     */
    public function the_email_is_required():void
    {
        $response = $this->post(route('login'), [
            'email' => null,
            'password' => 'dummy-password',
        ]);

        $response->assertSessionHasErrors(['email']);
        $this->assertGuest();
    }

    /**
     * @test
     */
    public function the_email_must_be_a_string() :void
    {
        $response = $this->post(route('login'), [
            'email' => 26.7,
            'password' => 'dummy-password',
        ]);

        $response->assertSessionHasErrors(['email']);
        $this->assertGuest();
    }

Que sucede aquí: Si observas tenemos una llamada a un método post ($this->post()) que imita el envío de datos del formulario al controller y le pasa los dos inputs que requiere el controller para validar; es decir el campo email y password.

Después de eso el método assertSessionHasErrors valida que en la sesión exista un error como forma de comprobar la validación.

Finalmente compruebo con el método assertGuest() que el usuario no logra hacer login en el sistema, si la validación falla.

Si ya lo notaste lo que hicimos con el campo email es necesario hacerlo también para el campo password! y si lo sé. puedes estar pensando

¿dónde esta el ahorro en esto?.

Pero tranquilo eso lo vamos a ver en un momento mas, pero de momento necesito que notes algo importante.

Seguro ya habrás sospechado que cada prueba que agreguemos vamos a necesitar escribir un nuevo método para validar ese nuevo caso. Por ejemplo si quieres validar que el campo email tiene el formato adecuado.

Esto haría que tuvieras que agregar una nuevo caso.

Input/PruebaRequiredStringemail
emailnull26.7invalid-email
passwordnull26.7dummy-password

Como se muestra en la tabla tendrías que agregar un nuevo método para los casos del campo email.

Ahora ¿Qué pasa si te piden “probar” con un string vacío ? pues como estas suponiendo tendrías que agregar otro método para probar ese nuevo caso!.

Muy probablemente estés pensando, este tipo se le voló un tornillo; mejor me regreso a las pruebas en el navegador!

Pero tranquilo existe otra forma de hacer esto sin morir en el intento.

Data Providers

Los Data Providers son métodos que regresan un arreglo que permite pasar datos a tus pruebas como si se recorrieran mediante un foreach, lo fantástico de esto es que solo necesitas crear el método, indicar donde se va a usar y decirle a la prueba como va a usar esos valores que va a recibir en cada interacción.

Como funciona el Data Provider

Déjame te muestro un ejemplo del manual de PhpUnit:

class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertSame($expected, $a + $b);
    }

    public function additionProvider()
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }
}

Si observas en el ejemplo se esta probando una suma, cada iteración pasa los valores $a, $b, $expected en el orden que tienen en el arreglo,

Es decir que para el primer caso la suma seria 0 + 0 = 0 con lo cual la prueba pasa sin problema por que la suma obtiene el resultado esperado de cero que corresponde a la tercera posición del primer arreglo ([0, 0, 0]) y esta siendo usado por la variable $expected.

Muy bien de esa misa forma va a recorrer los otros arreglos para hacer las comprobaciones del assertSame.

Para usar un Data Provider se usa la notación @dataProvider seguida del método que vamos a usar(additionProvider) como se muestra en el ejemplo de arriba.

Implementando data providers en nuestra comprobación

¿Cómo podemos usar esto en nuestra prueba?

Muy sencillo vamos, si observas el ejemplo que hicimos para validar el campo de E-mail debiste de haber notado que la prueba hace lo mismo pero solo cambian los valores en el arreglo que se pasa el método $this->post().

Esto me indica que es posible dos Data Providers para las pruebas, veamos como queda eso.

Creación de los data providers

public function invalidInputForPassword() : array
    {
        return [
            ['password' , null,'El campo contraseña es obligatorio.'],
            ['password' , 26.7, 'El campo contraseña debe ser una cadena de caracteres.'],
        ];
    }

    public function invalidInputForEmail(): array
    {
        return [
            ['email', null, 'El campo correo electrónico es obligatorio.'],
            ['email' , 26.7, 'El campo correo electrónico debe ser una cadena de caracteres.'],
        ];
    }

Los dos métodos regresan un arreglo donde la primera posición es el campo que voy a evaluar, el segundo va a ser el valor que quiero evaluar en la prueba y finalmente el mensaje de error que espero recibir si no pasa la validación.

Creación de comprobaciones para las validaciones

Muy bien para usarlos creare solo dos pruebas que usaran estos valores y con eso probaremos los casos originales, es decir el requerido y el que valida un string.

/**
 * @test
 * @dataProvider invalidInputForPassword
 */
public function user_cant_login_with_password_with_invalid_data($formInput, $formInputValue, $message):void
{
    $response = $this->post(route('login'), [
        'email' => 'name@localhost',
        'password' => $formInputValue,
    ]);

    $response->assertSessionHasErrors([$formInput => $message]);
}

/**
 * @test
 * @dataProvider invalidInputForEmail
 */
public function user_cant_login_with_email_with_invalid_data($formInput, $formInputValue, $message):void
{
    $response = $this->post(route('login'), [
        'email' => $formInputValue,
        'password' => 'dummy-password',
    ]);

    $response->assertSessionHasErrors([$formInput => $message]);
    $this->assertGuest();
}

¿Qué ganamos con esto?

que ahora para probar un nuevo caso, solo tenemos que agregarlo en el arreglo correspondiente.

y solo requieres tener dos métodos para realizar las comprobaciones.

Pero… si ya sé, siempre existe un pero y es que si agrego si agrego un nuevo campo al formulario me veo obligado a agregar un nuevo provider y un método para el nuevo campo.

Pero veamos como podemos solucionar esta situación.

Mejorando la comprobación

Si miras el código de arriba notaras que son los mismo valores y que son la misma cantidad de elementos los que se pasan en las dos pruebas, Así que es muy sencillo que tengamos una sola prueba usando dos providers!.

veamos como queda esto en código.

Primero tenemos que agregar una propiedad para loa valores por defecto que corresponden a dos casos validos cuando se esta probando cada campo.

private $formFields = [
        'email' => 'dummy@domain.com',
        'password' => 'dummy-password',
    ];

Esta propiedad me servirá para remplazar el campo que vamos a probar en cada iteración de los providers de la siguiente forma.

/**
     * @param $formInput
     * @param $formInputValue
     * @param $message
     * @test
     * @dataProvider invalidInputForEmail
     * @dataProvider invalidInputForPassword
     */
    public function user_cant_login_using_invalid_data($formInput, $formInputValue, $message) : void
    {
        $response = $this->post(
            route('login'),
            array_replace($this->formFields, [$formInput => $formInputValue])
        );

        $response->assertSessionHasErrors([$formInput => $message]);
    }

Como puedes observar solo se remplaza el campo que se requiere en cada caso. de esta forma podemos tener una sola prueba y varios providers con sus propios casos cada uno.

De esta forma si agrego un nuevo campo ahora solo requiero agregar el Provider correspondiente y sus casos de prueba.

Consideraciones adicionales.

Algo que debes de tener en cuenta es que es posible que en algunos casos no puedas llegar a este resultado de forma sencilla, sobre todo si tus provider no proporcionan los mismos argumento a la prueba.

Pero para casos en donde no podemos hacer esto, una opción es mover la validación a un FormRequest y hacer las pruebas de forma aislada para cada Request. Es posible usar Data Providers para estos escenarios.

Para finalizar te dejo nuevamente la pregunta del inicio:

¿Cómo compruebas tus validaciones?

deja tu comentario!