Testing a Laravel app with hundreds of migration files

And stay sane while doing it.

Testing Laravel is usually fast. You can use an in-memory database, and all your tests run hella fast; even going directly with MySQL is fast enough.

But when you inherit an application (rarely for good reasons), and you find out it has more than 370 migration files adding up to 120 tables, you know you’re in big trouble.

How the hell does one reaches up to 370 migration files anyway?

Well, by being successful.

This app makes a ton of money and dates back from four years ago, January 2016, and new features were added every month since then.

The reason why we inherited this project is because the former team, while they did move fast and build the entire thing in just a few months, tons of bugs and issues started to reach the surface, and their responsiveness wasn’t as sharp as in the beginning. Features took weeks to build, rather than hours as it was in the beginning.

Well, that’s the official answer. But we know better.

Our speed slows down as the app reaches a certain size, but going from “we can build this in a few hours” to “we need two weeks to add this option” usually means only one thing. Nobody ever wrote a single test for this application. Nada.

Then we came in and said, “Ok, we need to try and stabilize this thing first. Let’s start with writing some HTTP tests”.

Running our first test took like 25 seconds. 25 seconds to check if a delete request actually deleted something.

Our app was quite poorly written, but 25 seconds? Something weird was going on.

We found out that the reason was the large number of migration files – 30-50 migrations run fast, but as you go into the hundreds, it can take up to 20 seconds to run all of them.

The solution was, instead of dropping and re-create all the tables, we’d just truncate all the data. To do this, we override the refreshTestDatabase method from the RefreshDatabase trait.

protected function refreshTestDatabase()
{
    if (!RefreshDatabaseState::$migrated) {
        DB::statement("SET foreign_key_checks=0");
        $databaseName = DB::getDatabaseName();
        $tables = DB::select("SELECT * FROM information_schema.tables WHERE table_schema = '$databaseName'");

        foreach ($tables as $table) {
            DB::table($table->TABLE_NAME)->truncate();
        }

        DB::statement("SET foreign_key_checks=1");

        RefreshDatabaseState::$migrated = true;
    }

    $this->beginDatabaseTransaction();
}

One of the downsides was that we would need to keep our testing database structure in sync. Every time we added a new migration (sheesh), we’d need to update the testing database.

But that’s a small price to pay. Our test that used take 25s to run got down to just 1 second!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.