Running Laravel Tests in Parallel: A Guide for Faster CI Pipelines

If you're reading this article, kudos to you! 🎉
Writing tests is essential for building reliable software, and the fact that you're already doing it deserves recognition. Keep up the great work! 👏
As your Laravel application grows, so does your test suite. Running tests sequentially can become time-consuming, slowing down your development workflow. Fortunately, Laravel allows running tests in parallel, significantly reducing execution time. In this article, we'll explore how to run Laravel tests in parallel and what considerations to keep in mind.
Enabling Parallel Testing in Laravel
By default, Laravel and Pest/PHPUnit execute tests sequentially within a single process. However, you can speed up test execution by running them simultaneously across multiple processes. To enable parallel testing, follow these steps:
1. Install the Parallel Testing Package:
composer require brianium/paratest --dev
2. Run Tests in Parallel:
php artisan test --parallel
By default, Laravel will create as many processes as there are available CPU cores. You can specify the number of processes manually:
php artisan test --parallel --processes=4
3. Recreating Test Databases:
Laravel automatically handles database creation and migration for each parallel process, appending a unique process token to the database name (e.g., your_db_test_1
, your_db_test_2
). To recreate test databases before running tests, use:
php artisan test --parallel --recreate-databases
Considerations When Running Tests in Parallel
Parallel testing introduces new challenges, so your test suite must be structured correctly to avoid those problems. Here are key considerations:
1. Database Isolation
Each test process must use an isolated database instance to prevent conflicts.
-
Separate Databases Per Process: Laravel automatically creates separate test databases per process.
-
Use SQLite In-Memory Databases: This is the simplest way to ensure isolation.
-
Truncate Instead of Migrate: Using
RefreshDatabase
orDatabaseTruncation
strategies speeds up tests.
2. Handling Parallel Testing Hooks
You may need to set up or tear down specific resources for parallel execution. Laravel provides the ParallelTesting
facade to handle this:
namespace App\Providers;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\ParallelTesting;
use Illuminate\Support\ServiceProvider;
use PHPUnit\Framework\TestCase;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
ParallelTesting::setUpProcess(function (int $token) {
// Setup logic for each process
});
ParallelTesting::setUpTestCase(function (int $token, TestCase $testCase) {
// Setup logic for each test case
});
ParallelTesting::setUpTestDatabase(function (string $database, int $token) {
Artisan::call('db:seed');
});
ParallelTesting::tearDownTestCase(function (int $token, TestCase $testCase) {
// Cleanup logic per test case
});
ParallelTesting::tearDownProcess(function (int $token) {
// Cleanup logic per process
});
}
}
3. Accessing the Parallel Testing Token
Each test process has a unique process "token" that helps segment resources. You can retrieve it using:
$token = ParallelTesting::token();
This token is a unique string identifier assigned to each test process, helping to isolate resources across parallel executions. For instance, Laravel automatically appends this token to the names of test databases created for each parallel test process. Additionally, if your application has custom shared resources, such as caches, files, or session storage, you can use this token to further segment and isolate them, preventing conflicts between parallel test runs.
4. File System Conflicts
If your tests involve file operations, conflicts can arise due to concurrent writes.
-
Use
Storage::fake()
to isolate disk operations.class FileUploadTest extends TestCase { public function test_file_upload() { // Fake the storage disk Storage::fake('public'); // Create a fake file $file = UploadedFile::fake()->image('test-image.jpg'); // Perform a file upload request $response = $this->post('/upload', [ 'file' => $file, ]); // Assert the file was stored Storage::disk('public')->assertExists('uploads/test-image.jpg'); // Assert the response was successful $response->assertStatus(200); } }
-
If using
storage/app
, ensure each process has a unique directory.
5. Race Conditions in Jobs & Events
Parallel execution may cause unexpected behavior in queued jobs and events. Some things that could make your life easier:
-
Disable queues during tests:
QUEUE_CONNECTION=sync
-
Ensure event-driven behavior does not depend on execution order.
- Use
Event::fake()
to assert that events were fired without executing their listeners.
6. Caching and Session Issues
Parallel tests sharing the same cache or session store can interfere with each other.
Use an in-memory cache:
CACHE_DRIVER=array
Use an in-memory session driver:
SESSION_DRIVER=array
Improving Test Execution Time Further
In addition to parallel testing, you can speed up Laravel tests by:
-
Optimizing Database Indexes: Reducing query execution times if your tests invove handling much data.
-
Running Only Necessary Tests: Use
php artisan test --filter=MyTest
to execute a subset of tests separately.
Conclusion
Running Laravel tests in parallel can significantly improve performance, reducing execution time and making your CI/CD pipeline more efficient, and also improving your feedback loop locally while developing new features.
Just be mindful of any shared resources that are being used across tests, and you should be good to go.
Happy testing! 🤘