Skip to content

Why Your Test Suite Takes Eleven Minutes

A slow suite is a suite people skip. The time is almost never in the assertions - it is in rebuilding the database, hashing passwords, and reaching the network without meaning to.

4 min read

A suite that takes eleven minutes is not run before pushing. It is run by CI, after the context has been lost, which turns a five-second correction into a twenty-minute round trip. Eventually somebody pushes past a red build because they are fairly sure it is unrelated.

The speed of the suite is therefore not a developer comfort. It is what decides whether the tests do their job.

Measure before guessing

php artisan test --profile

This prints the slowest tests. Almost always a handful of cases account for most of the wall clock, and the fix is specific rather than architectural.

The database, which is usually most of it

Migrating for every test. RefreshDatabase wraps each test in a transaction and rolls it back, which is fast. DatabaseMigrations runs every migration for every test, which is not. On a project with two hundred migrations, that difference is the whole problem.

Migrations rather than a schema dump. Even once per run, replaying two hundred migrations takes longer than loading their result:

php artisan schema:dump --prune

The suite then loads one SQL file. This is the single biggest win on a mature codebase and it takes one command.

Factories creating more than the test needs. A factory with a has() chain creating twenty related records for a test that reads one field. Create the minimum, and use make() rather than create() where nothing touches the database.

Password hashing, which nobody expects

Hashing is deliberately slow - that is its purpose. A suite creating hundreds of users through a factory pays that cost hundreds of times.

// tests/TestCase.php or Pest.php
Hash::driver('bcrypt')->setRounds(4);

On suites with a lot of user fixtures this alone has cut runtimes in half.

Tests that reach the network

The most damaging category, because they are slow and unreliable at once. A test that calls a real API waits on somebody else's server and fails when that server has a bad day.

Http::preventStrayRequests();

Put that in the base test case. Any test making an unfaked HTTP call now fails immediately and tells you which one - and you will find some you did not know about.

The same applies to queues, mail and storage. Faked, they are instant; real, they are doing work the test is not asserting on.

Sleeps and polling

sleep(2) in a test waiting for something asynchronous is two seconds every run, forever, and it is either too long or - on a slower machine - not long enough.

Laravel's time helpers make waiting unnecessary: freeze time, travel forward, assert. For genuinely asynchronous work, run the queue synchronously in tests rather than waiting for a worker.

Browser tests, which belong in a different bucket

They are slow because they drive a real browser, and there is no trick that makes that fast. The answer is quantity rather than speed: a small number covering the paths that matter, run separately from the fast suite, so they do not sit between a developer and their feedback.

Parallelism, once the tests deserve it

php artisan test --parallel

Near-linear improvement on a multi-core machine - and it will expose every test that was quietly depending on another. Shared fixture files, hardcoded ids, a database record assumed to exist because an earlier test created it.

Those failures are worth having. A test that only passes when run after another is not a test, it is a sequence, and it was going to fail at random in CI anyway.

The target

Under a minute for the suite people run before pushing, with anything slower

  • browser tests, integration against real services - in a separate job that runs after.

The measure is not coverage or elegance. It is whether running the tests is something a developer does without deciding to.

A suite nobody runs is also a suite nobody can upgrade behind. That is why building one is the first phase of an upgrade and not the last. And if the fast suite runs on SQLite while production runs MySQL, that trade has a cost, and it is not always a small one.

Related questions

How fast should a suite be?
Fast enough that running it is not a decision. Under a minute and people run it before every push without thinking; past five they start pushing and waiting for CI, which moves the feedback from seconds to minutes and costs far more than the time saved.
Is an in-memory database the answer?
It is fast and it is a different database from the one you run in production, which means it will not catch the constraint violation, the collation problem or the migration that locks. Reasonable for a unit-heavy suite; risky as the only thing standing between you and a schema mistake.
Does parallel testing help?
Substantially, and only once the tests are isolated enough to survive it. Anything writing to a shared fixture path, a fixed record id or an external service will fail intermittently under parallelism - which is worth fixing anyway, because those are the tests that also fail at random in CI.
Where should we look first?
At the slowest ten tests, not at the average. Suites are usually dominated by a handful of cases doing something expensive, and fixing those is an afternoon. Rewriting the whole suite for speed is not.

← Back to all articles

Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page