Componentes para Laravel

Testbench es un componente que nos permite realizar pruebas de paquetes para Laravel que requieran integración con el sistema de rutas, base de datos, vistas y más. En esta lección realizaremos nuestra primera prueba utilizando este componente.

Mira el código en GitHub: actual, resultado, comparación.

Eventualmente vamos a querer que nuestro componente retorne una vista, donde podamos colocar la lógica para generar el formulario HTML de manera ordenada.

Cambiemos el método render de la clase Form para que retorne una vista:

<?php

namespace Styde;

class Form
{
    public function render()
    {
        return view('form');
    }
}

Nuestra prueba falla estrepitosamente:

1) Tests\FormTest::renders_a_form
Illuminate\Contracts\Container\BindingResolutionException: Target [Illuminate\Contracts\View\Factory] is not instantiable.

Esto sucede porque la función view utiliza el contenedor de inyección de dependencias de Laravel para crear una instancia de la interfaz Illuminate\Contracts\View\Factory. Si has visto nuestro Curso de programación orientada a objetos con PHP sabrás que no es posible instanciar una interfaz; por lo tanto el error: [Illuminate\Contracts\View\Factory] is not instantiable.

Si revisamos con calma veremos que la clase en vendor/laravel/framework/src/Illuminate/View/ViewServiceProvider.php se encarga de crear una instancia de la clase \Illuminate\View\Factory y registrarla en el contenedor de inyección de dependencias bajo el nombre de servicio view. Nota que se registra una clase, no una interfaz:

<?php

namespace Illuminate\View;

//...

class ViewServiceProvider extends ServiceProvider
{
    //...

    public function registerFactory()
    {
        $this->app->singleton('view', function ($app) {
            //...

            $factory = $this->createFactory($resolver, $finder, $app['events']);

            //...

            return $factory;
        });
    }

    protected function createFactory($resolver, $finder, $events)
    {
        return new Factory($resolver, $finder, $events);
    }

    //...
}

Luego, en el método registerCoreContainerAliases de vendor/laravel/framework/src/Illuminate/Foundation/Application.php, se registran los alias \Illuminate\View\Factory::class y \Illuminate\Contracts\View\Factory::class para el servicio view:

<?php

namespace Illuminate\Foundation;

// ...

class Application extends Container /* ... */
{
    /**
     * Register the core class aliases in the container.
     *
     * @return void
     */
    public function registerCoreContainerAliases()
    {
        foreach ([
            // ...
            'view' => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class],
        ] as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }
}

Es por esto que cuando el contenedor de inyección de dependencias recibe la «orden» de crear una instancia de la interfaz \Illuminate\Contracts\View\Factory::class se crea o se devuelve la instancia del servicio view, tal como es configurada en ViewServiceProvider. Sin embargo, puesto que aquí no estamos dentro del contexto de una aplicación de Laravel, esto no va a funcionar. Apliquemos una solución:

Utilizando Orchestra TestBench

Vamos a cambiar la clase TestCase, que creamos en la lección anterior, para que extienda de \Orchestra\Testbench\TestCase:

<?php // tests/TestCase.php

namespace Tests;

class TestCase extends \Orchestra\Testbench\TestCase
{

}

Puesto que \Orchestra\Testbench\TestCase extiende de PHPUnit\Framework\TestCase, aún podemos ejecutar las pruebas tal como antes.

Si ahora re-ejecutamos la prueba podremos ver que… Falla. Pero el error es diferente y más cercano a nuestro objetivo:

1) Tests\FormTest::renders_a_form
InvalidArgumentException: View [form] not found.

La prueba falla porque la vista form no es encontrada. ¡Esto quiere decir que se creó con éxito el servicio de vistas! Pero Laravel no puede encontrar la vista form. Creemos los directorios resources/views con la vista form.blade.php:

<!-- resources/views/form.blade.php -->
<form></form>

Re-ejecutemos la prueba. Nope. Laravel aún no sabe cómo cargar nuestra vista.

1) Tests\FormTest::renders_a_form
InvalidArgumentException: View [form] not found.

Registrar Service Providers con TestBench

Testbench nos permite registrar y ejecutar Service Providers personalizados. Dentro de un Service Provider podemos registrar archivos de configuración, directorios de vistas y más. Para esto, solo tenemos que sobrescribir el método getPackageProviders en nuestra clase de prueba o dentro de TestCase:

<?php // tests/TestCase.php

namespace Tests;

class TestCase extends \Orchestra\Testbench\TestCase
{
    protected function getPackageProviders($app)
    {
        return ['Styde\FormServiceProvider'];
    }
}

Dentro del método getPackageProviders retornamos los proveedores que queremos registrar, similar a como lo haríamos dentro de config/app.php en una aplicación normal de Laravel.

Al re-ejecutar la prueba obtendremos el siguiente error:

1) Tests\TestUnitTest::test_phpunit_test
Error: Class ‘Styde\FormServiceProvider’ not found

Vamos a solucionarlo creando la clase dentro de un nuevo directorio src:

<?php // src/FormServiceProvider.php

namespace Styde;

class FormServiceProvider
{

}

Al re-ejecutar la prueba obtenemos este error:

1) Tests\FormTest::renders_a_form
Error: Call to undefined method Styde\FormServiceProvider::isDeferred()

Esto es porque nuestro proveedor de servicios debe extender de Illuminate\Support\ServiceProvider:

<?php // src/FormServiceProvider.php

namespace Styde;

use Illuminate\Support\ServiceProvider;

class FormServiceProvider extends ServiceProvider
{

}

Con esto la prueba debería volver al error anterior:

1) Tests\FormTest::renders_a_form
InvalidArgumentException: View [form] not found.

Hasta aquí todo bien. Vamos a volver a FormServiceProvider y desde allí declaremos el método boot, dentro de este método vamos a configurar nuestro directorio de vistas.

<?php

namespace Styde;

use Illuminate\Support\ServiceProvider;

class FormServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'styde-form');
    }
}

Como primer argumento de loadViewsFrom hacemos referencia al directorio donde están las vistas del componente, como segundo argumento debemos colocar el nombre del componente.

Con esto la prueba seguirá con el mismo error, la vista form no es encontrada. Necesitamos indicar que queremos cargar la vista form del paquete styde-form:

<?php // src/Form.php

namespace Styde;

class Form
{
    public function render()
    {
        return view('styde-form::form');
    }
}

Nota la sintaxis de dobles dos puntos :: con la cual dividimos el nombre del paquete del nombre de la vista que queremos cargar dentro de dicho paquete. De esta manera cada componente puede incluir sus vistas propias sin que los nombres colisionen con las vistas de otros paquetes o de la aplicación como tal. Más adelante podremos exportar y sobrescribir la vistas en cada una de la aplicaciones que usen styde-form, si así lo requerimos.

El próximo error es aún más críptico, falla comprobando que [una serie de código] es igual a <form></form>. Esto se produce porque el helper view retorna un objeto de la clase \Illuminate\View\View. Para solucionar este problema podemos llamar al método render o toHtml:

<?php // Extracto de tests/FormTest.php

$form = new Form;

$this->assertSame('<form></form>', $form->render()->render());
// o:
$this->assertSame('<form></form>', $form->render()->toHtml());

Esta sintaxis puede parecer un poco extraña, lo que sucede es que cuando llamamos al método render por primera vez estamos retornando una vista que es un objeto de la clase \Illuminate\View\View. Para obtener la cadena de HTML retornada por la vista debemos llamar al método render nuevamente o al método toHtml de dicha clase View.  Más adelante vamos a mejorar esta sintaxis; por ahora veamos porqué la prueba sigue fallando:

Failed asserting that two strings are identical.
— Expected
+++ Actual
@@ @@
-‘<form></form>’
+'<form></form>
+’

Esto puede suceder si tu editor agrega una línea vacía al final del archivo. Vamos a utilizar trim para eliminar cualquier carácter o caracteres vacíos al principio o al final de la vista retornada:

<?php // Extracto de tests/FormTest.php

$form = new Form;

$this->assertSame('<form></form>', trim($form->render()->toHtml()));

¡Con esto la prueba pasa! En la siguiente lección vamos a mejorar esta sintaxis y a continuar desarrollando nuestro componente con opciones adicionales.

Regístrate hoy en Styde y obtén acceso a todo nuestro contenido.

Lección anterior Realizando nuestra primera prueba con PHPUnit Lección siguiente Pruebas de integración para componentes de Blade en Laravel 7 o superior