Speed Up Your Laravel Test Suite: The Cache Testing Traits You've Been Waiting For.
November 20, 2025
7 minutes
Right, let's have a chat about test suite performance. You know that feeling when you push your code, then go make a brew while waiting for the CI pipeline to finish? Yeah, we've been there too. Laravel 12.38 just dropped two new testing traits that might just save you enough time to actually drink that tea while it's still hot.
Meet WithCachedRoutes and WithCachedConfig - two deceptively simple traits that can cut your test suite run time dramatically.
The Hidden Cost of Feature Tests
We all know that writing feature tests is important and the more test coverage you have in your application the safer you will sleep at night. But here's something that might surprise you: every time one of your feature tests runs, Laravel rebuilds your entire application from scratch. Every. Single. Test.
We're talking about:
Loading every service provider
Reading and parsing every route file
Processing every configuration file
Registering all your middleware
Setting up all your bindings
Now multiply that by however many tests you have. Got 500 tests? That's 500 times your application is being completely rebuilt. No wonder your test suite takes forever.
Why We Never Noticed Before
When Laravel applications are small, this overhead is negligible. Your typical starter app with a dozen routes and the default config files? The bootstrap time is maybe minimal. Not worth thinking about.
But then your application grows. You add an API. Then an admin panel. Maybe you split things into modules for different features. You integrate with third-party services, each with their own config files. Before you know it, you've got:
15 route files (web, api, admin, webhooks, internal...)
30+ config files
Multiple service providers registering components
Route model bindings everywhere
Global middleware stacks
Suddenly that 50ms bootstrap time is 200ms. Still doesn't sound like much? Let's do the maths: 500 tests × 200ms = 100 seconds. That's over a minute and a half of your test suite just booting Laravel, not actually testing anything.
Enter the Caching Traits
This is where Laravel 12.38's new traits come in (huge props to Luke Kuzmish who contributed these to core). Instead of rebuilding everything for each test, these traits build your routes and config once, then reuse them for every test in your suite.
Using WithCachedRoutes
The simplest approach? Add it to your base test case:
1<?php 2 3namespace Tests; 4 5use Illuminate\Foundation\Testing\TestCase as BaseTestCase; 6use Illuminate\Foundation\Testing\WithCachedRoutes; 7 8abstract class TestCase extends BaseTestCase 9{10 use WithCachedRoutes;11}
That's it. Every test that extends your TestCase now benefits from cached route registration.
Adding WithCachedConfig
Same story with configuration:
1<?php 2 3namespace Tests; 4 5use Illuminate\Foundation\Testing\TestCase as BaseTestCase; 6use Illuminate\Foundation\Testing\WithCachedRoutes; 7use Illuminate\Foundation\Testing\WithCachedConfig; 8 9abstract class TestCase extends BaseTestCase10{11 use WithCachedRoutes;12 use WithCachedConfig;13}
For the Pest Enthusiasts
Using Pest? Even easier - just add them globally in your Pest.php file:
1<?php2 3use Illuminate\Foundation\Testing\WithCachedRoutes;4use Illuminate\Foundation\Testing\WithCachedConfig;5 6pest()->use(WithCachedRoutes::class, WithCachedConfig::class);
Now every test benefits without touching a single test file.
Example: API Test Suite
Let me show you a scenario you might encounter frequently. You're building an e-commerce platform with separate route files for different concerns:
1<?php 2 3// routes/api/products.php 4Route::prefix('products')->group(function () { 5 Route::get('/', [ProductController::class, 'index']); 6 Route::get('/search', [ProductController::class, 'search']); 7 Route::get('/{product}', [ProductController::class, 'show']); 8 Route::post('/', [ProductController::class, 'store']); 9 Route::put('/{product}', [ProductController::class, 'update']);10 Route::delete('/{product}', [ProductController::class, 'destroy']);11});12 13// routes/api/orders.php14Route::prefix('orders')->group(function () {15 Route::get('/', [OrderController::class, 'index']);16 Route::post('/', [OrderController::class, 'store']);17 Route::get('/{order}', [OrderController::class, 'show']);18 Route::post('/{order}/fulfill', [OrderController::class, 'fulfill']);19 Route::post('/{order}/cancel', [OrderController::class, 'cancel']);20});21 22// routes/api/customers.php23// ... another dozen routes24 25// routes/admin.php26// ... 50+ admin routes
Your test might look like this:
1<?php 2 3namespace Tests\Feature\Api; 4 5use App\Models\Product; 6use Illuminate\Foundation\Testing\RefreshDatabase; 7use Tests\TestCase; 8 9class ProductApiTest extends TestCase10{11 use RefreshDatabase;12 13 /** @test */14 public function can_list_products()15 {16 Product::factory()->count(3)->create();17 18 $this->getJson('/api/products')19 ->assertOk()20 ->assertJsonCount(3, 'data');21 }22 23 /** @test */24 public function can_search_products()25 {26 Product::factory()->create(['name' => 'Laravel T-Shirt']);27 Product::factory()->create(['name' => 'PHP Mug']);28 29 $this->getJson('/api/products/search?q=laravel')30 ->assertOk()31 ->assertJsonCount(1, 'data');32 }33 34 /** @test */35 public function can_create_product()36 {37 $this->postJson('/api/products', [38 'name' => 'New Product',39 'price' => 29.9940 ])41 ->assertCreated();42 43 $this->assertDatabaseHas('products', [44 'name' => 'New Product'45 ]);46 }47}
Without the caching traits, each of these three tests loads and parses all your route files. With the traits? Once for the entire test class.
The Performance Impact
We've heard from developers out there that they are seeing impressive results across different application types:
SaaS application with 200 routes: Test suite down from 3:20 to 2:15
API with 80+ endpoints: Tests run in half the time
Small application with 500 tests: Still saved 15-20 seconds
I myself have just implemented this on a small internal project that I've recently upgraded to Laravel 12 that I'm trying to resurrect, it doesn't have a huge code base and a huge test suite it currently has 543 tests, implementing these two traits in the Pest test suite globally cut the test time down by 2seconds taking it from 15s to 13 seconds which might not sound like a lot but over time that really does add up.
The improvements are even more dramatic with parallel testing, as each process benefits from the caching.
What About Laravel's Built-in Caching?
"But wait," you might say, "can't I just use php artisan route:cache?"
Well, yes and no. The problem with using Laravel's built-in caching commands for tests is:
You'd need to remember to run
php artisan route:cache --env=testingbefore testsThe cached files can interfere with your local development
Different developers might have different cached states
Your CI pipeline needs extra setup steps
These traits solve all that by keeping everything in memory, just for the duration of your test run. No files to manage, no cache to clear, no confusion.
When Should You Use These Traits?
The short answer? Pretty much always, unless you have a specific reason not to. These traits are particularly beneficial for:
Modular monoliths where each module registers its own routes and config
Large applications with many route files or extensive configuration
Package development when you're loading multiple service providers
CI/CD pipelines where every second counts
Local development when you're running tests frequently
The Gotchas (Because There's Always Something)
While these traits are fantastic, there are a couple of edge cases to be aware of:
Dynamic Route Registration
If you have tests that modify route registration based on test conditions, the cached routes won't reflect those changes:
1<?php 2// This won't work as expected with WithCachedRoutes 3public function setUp(): void 4{ 5 parent::setUp(); 6 7 if ($this->shouldRegisterAdminRoutes()) { 8 Route::prefix('admin')->group(function () { 9 // Dynamic routes based on test conditions10 });11 }12}
In these rare cases, simply don't use the trait for that specific test class.
Configuration Mutations
Similarly, if you're modifying configuration values that affect how the application boots:
1<?php 2// Be careful with this pattern when using WithCachedConfig 3public function setUp(): void 4{ 5 parent::setUp(); 6 7 // This change won't affect the cached config 8 config(['app.providers' => array_merge( 9 config('app.providers'),10 [TestServiceProvider::class]11 )]);12}
For tests that need to modify boot-level configuration, skip the caching trait.
A Practical Testing Strategy
Here's how we're approaching it:
Add traits to base TestCase - Get the benefits everywhere by default
Run your test suite - Check everything still passes
Benchmark the improvement - Measure the time saved
Handle exceptions - Remove traits from specific test classes if needed
Most applications will see improvements with zero changes to actual test code.
The Technical Bit
For those curious about the implementation, these traits use static properties to store the built routes and configuration. When the first test runs, it builds everything normally but stores the result. Subsequent tests skip the building phase and use the stored version.
It's clever because it:
Requires no configuration
Works with parallel testing
Doesn't create any files
Automatically cleans up after your tests finish
Beyond Integration Tests
As Luke wisely notes in his article, while these traits provide huge wins for integration tests, remember that unit tests will always be faster:
"Using a test which boots and tears down the application will always be slower than a plain old PHPUnit test. Anything that can be tested via a unit test should be, but for the high confidence that integration tests can offer, these traits are a huge win."
So yes, use these traits, but also remember to:
Write unit tests where possible
Use
Factory::make()when database writes aren't necessaryBe mindful of model events that trigger queries
Test single units of code when appropriate
The Bottom Line
Look, nobody enjoys waiting for tests to run. These traits won't make your tests instant, but they should make them noticeably faster. And faster tests mean you'll run them more often, catch bugs earlier, and spend less time staring at loading bars.
The best part? It takes literally 30 seconds to implement. Add the traits, run your tests, enjoy the speed boost. It's the kind of quick win we all need.
Try It Today
Laravel 12.38 is available now, so there's nothing stopping you from trying these traits today:
1composer update laravel/framework
Add the traits to your base TestCase class, run your benchmarks, and prepare to be impressed. We'd love to hear what kind of improvements you're seeing.
And remember, faster tests mean you'll run them more often, which means you'll catch bugs earlier, which means happier clients and fewer late-night debugging sessions. Everyone wins.
Looking to start your next Laravel project?
We’re a passionate, happy team and we care about creating top-quality web applications that stand out from the crowd. Let our skilled development team work with you to bring your ideas to life! Get in touch today and let’s get started!