Speeding up Unit Tests with Laravel

We suffered the slowness of the unit testing that was integrated with Laravel migration until last week. The tests execution time on my Mac is 21 minutes in average and more than one hour on our DEV server. we are annoyed with the huge time occupation in each release. Because we may deploy serval times one day on DEV.

The code structure for tests is simple. We have an abstract base test class named TestCase with some common functions. This class extends the BaseTest which is created as a root test class in Laravel. So other test classes extend the TestCase class will inherit all the public function provided by Laravel.

To keep test cases in a divided environment, we used the DatabaseMigrations trait to initialize the DB. The problem is this trait will be called before and after each test. Let me show you the code first.

public function runDatabaseMigrations()
{
    $this->artisan('migrate');

    $this->app[Kernel::class]->setArtisan(null);

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

 The first line in this function is the same as running "php artisan migrate" in the terminal. The more migration files you have, the more time you have to spend on the unit testing.

The third line is to do a job as the method name mentioned that it’s to run the rollback command before the application is destroyed.

To resolve this problem, I added a new trait for testing named DatabaseCachedMigrations. In the unit testing, we do not need to use rollback. We want to empty the DB directly and the feature of rollback is useless at here. To speed up the rollback is quite easy by using the MySQL code below.

SET FOREIGN_KEY_CHECKS = 0;
SET GROUP_CONCAT_MAX_LEN=32768;
SET @tables = NULL;
SELECT GROUP_CONCAT('', table_name, '') INTO @tables
FROM information_schema.tables
WHERE table_schema = (SELECT DATABASE());
SELECT IFNULL(@tables,'dummy') INTO @tables;
SET @tables = CONCAT('DROP TABLE IF EXISTS ', @tables);
PREPARE stmt FROM @tables;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET FOREIGN_KEY_CHECKS = 1;

To simplify the migration part is a little bit complex, but not hard. The purpose is not to execute the migration part without affecting the independence of each test. The solution is to cache the database schema. The steps to cache migration:

  1. Check whether the database schema is cached and not expired before a test case running.

  2. If the schema is valid, just import it to DB.

  3. If is true, return. If not, go to step 4.

  4. Run the $this->artisan('migrate').

  5. Cache the database schema to a file

  6. Empty the database

  7. Migration is cached successfully.

The effect of applying this to the unit testing is amazing, see the image below. The time decreased from 3s to less than 1s for a single test case. 

unit-test-execution-time

The test result on DEV is that the time was down to 10 minutes from more than 1 hour.