Seeding the database for integration tests in Laravel

In my last post I wrote about how to define the test environment for database integration tests. Now I want to describe, how the database can be populated with test or stam data.

First of all, every test inherited from the generated TestCase class executes the Artisan migrate command (TestCase::prepareForTests()). The trait Illuminate\Foundation\Testing\DatabaseMigrations is only required, if all tables of the schema have be dropped after the test execution. This could be necessary for system tests in which the whole schema is populated with test data. For “simple” integration tests using transactions should be sufficient.
One word about the traits DatabaseMigrations and DatabaseTransactions: Both are executed by PHPUnit before every test method. PHPUnit scans all methods in the test class for the documentation annotations @before, @after, @beforeClass and @afterClass (PHPUnit_Framework_TestCase::runBare() and PHPUnit_Util_Test::getHookMethods()). The traits both uses the @before annotation to setup the context:

trait DatabaseMigrations
{
    /**
     * ckl: this annotation advises PHPUnit to run the trait before every test case
     * @before
     */
    public function runDatabaseMigrations()
    {
        $this->artisan('migrate');

        $this->beforeApplicationDestroyed(function () {
            $this->artisan('migrate:rollback');
        });
    }
}

With this in mind the seeding of the database can be placed on two different locations. First of all, the non-working approaches. Putting the seeding inside the setUp() or prepareForTests() method does not work, because at runtime there is no active transaction:

    public function setUp() {
        parent::setUp();
        // ckl: wrong place; this method is called on startup. The seeding is outside an active transaction
        // $this->seed('QualificationsTableSeeder');
    }

    public function prepareForTests() {
        $sut = $this->app->make('\App\Services\ExperienceService');
        // ckl: wrong place; this method is called on startup. The seeding is outside an active transaction
        // $this->seed('QualificationsTableSeeder');
    }

Using a pure seeder method with @before does although not work. ReflectionClass::getMethods(), which is used by PHPUnit, returns at first all “native” / non-traited methods and after that the traited methods:

    /**
     * ckl: local methods have higher precedence than traits, so this method is called *before* the DatabaseTransactions trait has been called
     * @before
     */
    public function seedTables() {
        $this->seed('MyTableSeeder');
        $testInstance = factory('App\User')->create();
    }

By starting a transaction inside the seedTables(), we have the seeding inside a running transaction:

    /**
     * @before
     */
    public function seedTables() {
        // already start the transaction and let DatabaseTransactions only rollback the transaction. This does only work on the first test case
        DB::beginTransaction();
        $this->seed('MyTableSeeder');
    }

The rollback is done by the DatabaseTransactions trait. It is no problem having two DB::beginTransaction() calls and only one rollback. MySQL does not allow nested transactions, so Laravel starts only the transaction on the first DB::beginTransaction() call. Every other invoke only increments a counter.
Laravel only executes the rollback, if all DB::rollBack() methods have been called. The seedTables() has to look like

    /**
     * @before
     */
    public function seedTables()
    {
        DB::beginTransaction();

        $this->beforeApplicationDestroyed(function () {
            $this->app->make('db')->rollBack();
        });

        $this->seed('MyTableSeeder');
    }

A much cleaner solution is to call the seedTables() method inside every test case:

    public function seedTables() {
        $this->seed('MyTableSeeder');
    }

    public function testSeeding() {
        // ckl: transaction has been started
        $this->seedTables();

        $this->assertTrue(true);
    }