Laravel testing con múltiples bases de datos y seeders con integración continua en Gitlab

Laravel Multidatabase Testing with Gitlab CI

Algunas veces se presentan proyectos que nos complican las cosas, y uno de esos escenarios, sería el hacer en Laravel Testing con múltiples bases de datos y seeders y realizando integración continua en Gitlab.

Escenario

Nuestra aplicación necesita mas de una conexión o base de datos. Esto en local no tiene problema. EL problema se complica, porque tenemos que usar Gitlab CI con Gitlab Runners y tiene que haber mas de una base de datos, y la imagen oficial de Mysql para Docker no permite de más de una base de datos, aunque hay posibilidades de hacerlo al final, ¿no es matar la mosca a cañonazos?

En determinados escenarios, sin seeding la cosa es más sencilla. Te avisaremos a lo largo del desarrollo.

  • Necesitamos N bases de datos
  • En algunos casos hay que hacer seeding en alguna tabla de esas bases de datos.
  • Haremos testing y CI, en local y en Gitlab

Configurando el proyecto de Laravel

Para hacer Laravel Testing en un proyecto los datos deben ser adaptados a tu conexión local, de producción, etc. No hagas copy & paste sin leer y comprender lo que haces. Si lo deseas hay un artículo anterior, Laravel Ci con gitlab, Docker Runner, MySQL 8 y PHP 7.3 que podría ayudarte a comprender el mecanismo para trabajar con Gitlab CI

Config/database

Aquí necesitamos varias cosas:

  • Añadir N nombres de conexión por defecto
  • Añadir esas conexiones al arreglo connections
  • Añadir esas conexiones en formato testing en el arreglo connections

Para ello haremos uso de las configuración, las variables de entorno, y los prefijos, siendo estos últimos los que nos permitirán trabajar con una sola base de datos, de forma fácil.

Los elementos entre <> son para personalizar y los que no pueden personalzarse pero siendo cuidadosos y personalizarlos igual en cada lugar. Un equivoco puede dar mucho squebraderos de cabeza

return [

    'default' => env('DB_CONNECTION', 'mysql'),
    'connection_1' => env('DB_CONNECTION_1', 'db1'),
    'connection_n' => env('DB_CONNECTION_N', 'dbn'),
    
    'connections' => [

        'mysql' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => env('DB_PREFIX', ''),
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

        'db1' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE_1', 'forge'),
            'username' => env('DB_USERNAME_1', 'forge'),
            'password' => env('DB_PASSWORD_1', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => env('DB_PREFIX_1', ''),
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],
        'dbn' => ...

Ahora  también necesitaremos las entradas para la conexiones de testing en memoria con sqlite.

// PHPunit testing main connection
        'testing' => [
            'driver' => 'sqlite',
            'database' => ':memory:',
            'prefix' => 'default_',
        ],
        // PHPunit testing audit connection
        'testing_1' => [
            'driver' => 'sqlite',
            'database' => ':memory:',
            'prefix' => 'db1_',
        ],

Fichero .env

Ahora editaremos nuestro fichero .env para nuestra app

DB_CONNECTION=mysql
DB_HOST=<host>
DB_PORT=<3306>
DB_DATABASE=<database>
DB_USERNAME=<user>
DB_PASSWORD=<PaSsWoRd>
DB_PREFIX=<db0_>

DB_CONNECTION_VMAIL=connection_1
DB_HOST_VMAIL=<host>
DB_PORT_VMAIL=<3306>
DB_DATABASE_VMAIL=<database2>
DB_USERNAME_VMAIL=<user2>
DB_PASSWORD_VMAIL=<PaSsWoRd2>
DB_PREFIX_VMAIL=<db1_>

... 

 

.env.example (para uso con Gitlab Runners)

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=database_test
DB_USERNAME=<user>
DB_PASSWORD=<PaSsWoRd>
DB_PREFIX=<prefixdb0_>

DB_CONNECTION_VMAIL=connection_1
DB_HOST_VMAIL=mysql
DB_PORT_VMAIL=3306
DB_DATABASE_VMAIL=database_test
DB_USERNAME_VMAIL=<user>
DB_PASSWORD_VMAIL=<PaSsWoRd>
DB_PREFIX_VMAIL=<prefixdb1_>

Migraciones (migrations)

Al momento de escribir este artículo no encontré mejor forma de hacer Laravel Testing con estas características y estaba algo saturado, así que es muy posible que exista un mejor método de mejorar las modificaciones en las migraciones.

El proceso propuesto, pasa por indicar en cada clase de migraciones cual es la conexión a usar, y esto lo haremos así, usando en el constructor la asignación de la conexión y modificando la forma en la que usamos el método create(), haciéndolo desde la clase heredada Schema:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;

class CreateUsersTable extends Migration
{
    protected $schema;

    public function __construct()
    {
        $this->schema = Schema::connection(config('database.default'));
    }

    public function up()
    {
        $this->schema->create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            . . .
            . . .

            $table->timestamps();
        });
    }

    public function down()
    {
        $this->schema->dropIfExists('users');
    }
}

En el caso de una migración con otra conexión usaríamos el valor adecuado en la construcción de la clase

public function __construct()
{
   $this->schema = Schema::connection(config('database.connection_1'));
}

Relleno de datos (seeding)

En el relleno de datos usando seeding debemos tener en cuenta la conexion sobre la que trabajamos en cada sembrador

// Ejemplo 1
public function run()
{
   DB::connection(config('database.default'))->table('admin')->insert([...

// Ejemplo 2
public function run()
{
   DB::connection(config('database.db1'))->table('insecondb')->insert([...

Ejemplo con seeding basado en mysql

Puede ser que por necesidad necesitemos hacer sembrado desde un fichero sql. Esto nos obliga a tener dos ficheros .sql ya que la clausula INSERT INTO en un caso deberá tener el nombre de la tabla conforme a producción, y otra conforme a testing en Gitlab CI con otro prefijo

<?php

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

//use Illuminate\Support\Facades\DB;

class DbSchemasSeeder extends Seeder
{

    public function run()
    {

        if (env('DB_HOST') == 'mysql') {
            $sql = file_get_contents(database_path('seeds/fichero_para_testing.sql'));
            DB::connection(config('database.db1'))->statement($sql);
        } else {
            $sql = file_get_contents(database_path('seeds/fichero_para_local.sql'));
            DB::connection(config('database.default'))->statement($sql);
        }
    }
}

 

Enlaces interesantes

Agradecimientos

Gracias a Bolduc por su imagen tomada de Unsplash y a Canva por su app para editar

Comparte este articulo en

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax