Franklin Rivero
Franklin Rivero

Reputation: 631

How to Migrate and seed before the full test suite in Laravel with in memory database?

I'm trying to set up the test environment in my Laravel project. I'm using http://packalyst.com/packages/package/mayconbordin/l5-fixtures with json for the seeding with a sqlite in memory database and calling:

Artisan::call('migrate');
Artisan::call('db:seed');

in my setUp function but this is executed before every single test which it can grow to thousands in this project.

I tried the setUpBeforeClass but it didn't work. I think there because the createApplication method is called in every test and that reset the whole application and also wasn't loading the fixtures from the json probably for the same reason.

Upvotes: 21

Views: 30322

Answers (6)

bilogic
bilogic

Reputation: 667

I think the concept of https://stackoverflow.com/a/38591471/3872647 is correct, i.e. seed as little times as possible, but overriding code this way will likely lead to incompatiblities in the future which trips up new developers.

This post describes a better way by setting TestCase::$seed = true, where seeding is only done once per test class https://masteringlaravel.io/daily/2023-12-04-you-dont-need-to-manually-run-seeders-in-your-tests

Upvotes: 0

Franklin Rivero
Franklin Rivero

Reputation: 631

This is how I did it in case someone else is struggling with the same, I created a base testClase class that inherits from Laravel's and did this:

/**
 * Creates the application.
 *
 * @return \Illuminate\Foundation\Application
 */
public function createApplication()
{
    return self::initialize();
}

private static $configurationApp = null;
public static function initialize(){

    if(is_null(self::$configurationApp)){
        $app = require __DIR__.'/../bootstrap/app.php';

        $app->loadEnvironmentFrom('.env.testing');

        $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

        if (config('database.default') == 'sqlite') {
            $db = app()->make('db');
            $db->connection()->getPdo()->exec("pragma foreign_keys=1");
        }

        Artisan::call('migrate');
        Artisan::call('db:seed');

        self::$configurationApp = $app;
        return $app;
    }

    return self::$configurationApp;
}

public function tearDown()
{
    if ($this->app) {
        foreach ($this->beforeApplicationDestroyedCallbacks as $callback) {
            call_user_func($callback);
        }

    }

    $this->setUpHasRun = false;

    if (property_exists($this, 'serverVariables')) {
        $this->serverVariables = [];
    }

    if (class_exists('Mockery')) {
        Mockery::close();
    }

    $this->afterApplicationCreatedCallbacks = [];
    $this->beforeApplicationDestroyedCallbacks = [];
}

I overwrote the createApplication() and tearDown() methods. I changed the first one to use the same $app configuration and remove the part of the teardown() where it flush $this->app.

Every other of my test has to inherit from this TestClass and that's it.

Everything else didn't work. This works even with in memory database, it's 100s times faster.

if you are dealing with user session, once you log the user in you will have to log him out in tear down, otherwise the user will be logged in because the app environment is never reconstructed or you can do something like this to refresh the application every time you want:

protected static $applicationRefreshed = false;

/**
 * Refresh the application instance.
 *
 * @return void
 */
protected function forceRefreshApplication() {
    if (!is_null($this->app)) {
        $this->app->flush();
    }
    $this->app = null;
    self::$configurationApp = null;
    self::$applicationRefreshed = true;
    parent::refreshApplication();
}

And add this to the tearDown() before the $this->setUphasRun = false;:

if (self::$applicationRefreshed) {
        self::$applicationRefreshed = false;
        $this->app->flush();
        $this->app = null;
        self::$configurationApp = null;
}

Upvotes: 19

Duy Nguyen
Duy Nguyen

Reputation: 364

In my case, I created .env.testing file which was copied from .env.example file. Then, I added database information into this file like this.

APP_ENV=testing
APP_KEY=<generate your app key>
...
DB_CONNECTION=sqlite
DB_DATABASE=:memory:

In the terminal, you can run the migration artisan command with option --env like this.

php artisan migrate:fresh --env=testing

Upvotes: 1

Kamil Kiełczewski
Kamil Kiełczewski

Reputation: 92347

create file in your project testrunner with this content (also prepare .env.testing file with testing environment variables) :

php artisan migrate:rollback --env=testing
php artisan migrate --env=testing --seed
vendor/bin/phpunit

And give permission to execute by command chmod +x testrunner and execute it by ./testrunner. Thats all :)

Upvotes: 5

markdwhite
markdwhite

Reputation: 2449

The main approach in the above solutions is to run all migrations for all tests. I prefer an approach to specify which migrations and seeds should run for each test.

It may be more worthwhile on big projects as this can reduce timings for tests by about 70% (using the sqlite in-memory DB as already explained above). For small projects, it's maybe a bit too much faffing about. But anyway...

Use these in TestCase:

/**
 * Runs migrations for individual tests
 *
 * @param array $migrations
 * @return void
 */
public function migrate(array $migrations = [])
{
    $path = database_path('migrations');
    $migrator = app()->make('migrator');
    $migrator->getRepository()->createRepository();
    $files = $migrator->getMigrationFiles($path);

    if (!empty($migrations)) {
        $files = collect($files)->filter(
            function ($value, $key) use ($migrations) {
                if (in_array($key, $migrations)) {
                    return [$key => $value];
                }
            }
        )->all();
    }

    $migrator->requireFiles($files);
    $migrator->runPending($files);
}

/**
 * Runs some or all seeds
 *
 * @param string $seed
 * @return void
 */
public function seed(string $seed = '')
{
    $command = "db:seed";

    if (empty($seed)) {
        Artisan::call($command);
    } else {
        Artisan::call($command, ['--class' => $seed]);
    }
}

Then call migrate() and seed as required in individual tests, eg:

    $this->migrate(
        [
            '2013_10_11_081829_create_users_table',
        ]
    );
    $this->seed(UserTableSeeder::class);

Upvotes: 1

Aine
Aine

Reputation: 2688

Option 1

How about setting up the database using a migration and seeds and then using database transcations? (https://laravel.com/docs/5.1/testing#resetting-the-database-after-each-test)

I wanted to be able to set up my test database via artisan like this:

$ php artisan migrate --database=mysql_testing
$ php artisan db:seed --database=mysql_testing

As you can guess, I'm using mysql, but I don't see why this shouldn't work for sqlite. This is how I do it.

config/database.php

First add the test database info to your config/database.php file, under your current database info.

'connections' => [
        'mysql' => [
            'driver'    => 'mysql',
            'host'      => env('DB_HOST', 'localhost'),
            'database'  => env('DB_DATABASE', 'forge'),
            'username'  => env('DB_USERNAME', 'forge'),
            'password'  => env('DB_PASSWORD', ''),
            'charset'   => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix'    => '',
            'strict'    => false,
        ],
        'mysql_testing' => [
            'driver'    => 'mysql',
            'host'      => env('DB_HOST', 'localhost'),
            'database'  => env('DB_TEST_DATABASE'),
            'username'  => env('DB_USERNAME', 'forge'),
            'password'  => env('DB_PASSWORD', ''),
            'charset'   => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix'    => '',
            'strict'    => false,
        ],
    ],

If you do it like this, don't forget to add DB_TEST_DATABASE to your .env file:

DB_DATABASE=abc
DB_TEST_DATABASE=abc_test

phpunit.xml

Any values set in the phpunit.xml file, under overwrite values given in the .env file. So we tell phpunit to use the "mysql_testing" database connection instead of the "mysql" database connection.

<?xml version="1.0" encoding="UTF-8"?>
<phpunit>
    ...
    <php>
        ...
        <env name="DB_CONNECTION" value="mysql_testing"/>
</php>

Test class

My test classes look like this:

class MyTest extends \TestCase
{
    use \Illuminate\Foundation\Testing\DatabaseTransactions;

    public function testSomething()
    {

Option 2

Here the database is reset before every test, which is why I prefer Option 1. But you might be able to get it to work the way you like.

I tried this once before, and it might work for you.

tests/TestCase.php Extend the test case, to load a new .env file, .env.testing

<?php

class TestCase extends Illuminate\Foundation\Testing\TestCase
{
    /**
     * The base URL to use while testing the application.
     *
     * @var string
     */
    protected $baseUrl = 'http://localhost';

    /**
     * Creates the application.
     *
     * @return \Illuminate\Foundation\Application
     */
    public function createApplication()
    {
        /** @var $app \Illuminate\Foundation\Application */
        $app = require __DIR__.'/../bootstrap/app.php';
        $app->loadEnvironmentFrom('.env.testing');

        $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

        return $app;
    }
}

.env.testing

Create this new .env file and add in the database details

APP_ENV=testing
APP_DEBUG=true
APP_KEY=xxx

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=abc_testing
DB_USERNAME=xxx
DB_PASSWORD=xxx

In the test class:

Use PDO to drop and recreate the database - easier than trying to truncate everything. Then use artisan to migrate and seed the database.

class MyTest extends TestCase
{
    public static function setUpBeforeClass()
    {
        $config = parse_ini_file(".env.testing");
        $username = $config['DB_USERNAME'];
        $password = $config['DB_PASSWORD'];
        $database = $config['DB_DATABASE'];
        $host = $config['DB_HOST'];

        // Create test database
        $connection = new PDO("mysql:host={$host}", $username, $password);
        $connection->query("DROP DATABASE IF EXISTS " . $database);
        $connection->query("CREATE DATABASE " . $database);
    }

    public function testHomePage()
    {
        Artisan::call('migrate');
        Artisan::call('db:seed');

        $this->visit('/')
             ->see('Home')
             ->see('Please sign in')
             ->dontSee('Logout');
    }

Upvotes: 3

Related Questions