Laravel Run Parallel Tests With MySQL In Docker
May 21, 2020 by Areg Sarkissian
In this article I will show you how to run phpunit tests using a MySQL test database server while avoiding issues related to database refresh and migrations when running tests in parallel.
When running tests in parallel using the RefreshDatabase
trait is problematic.
Parallel Feature tests using a MySQl
In the blog post Laravel Feature Tests With MySQL In Docker I described how we can connect to a MySQL test database server running in docker when running phpunit feature tests.
For reference I will repeat the configuration from that post below.
Here is the docker compose file:
version: "3.1"
services:
# this is the application database server used when running the app
myapp-mysql:
image: mysql:8.0
container_name: app-mysql
# persist data to data/mysql directory to save data accross container runs
volumes:
- ./data/mysql:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD="${DB_PASSWORD}"
# a database with the name of the MYSQL_DATABASE setting will be created on the first run of this container
- MYSQL_DATABASE="${DB_DATABASE}"
- MYSQL_USER="${DB_USERNAME}"
- MYSQL_PASSWORD="${DB_PASSWORD}"
ports:
# connect to this database server through port 8001 on localhost
- "8001:3306"
# this is the test database server used when running phpunit tests
myapp-mysql-test:
image: mysql:8.0
container_name: app-mysql-test
# no volume mapping required since we dont need the data to persist after container is shut down
environment:
- MYSQL_ROOT_PASSWORD="${DB_PASSWORD}"
# a database with the name of the MYSQL_DATABASE setting will be created on the first run of this container
- MYSQL_DATABASE="${DB_DATABASE}"
- MYSQL_USER="${DB_USERNAME}"
- MYSQL_PASSWORD=${DB_PASSWORD}"
ports:
# connect to this test database server through port 8011 on localhost
- "8011:3306"
The file has a MySQL service for app development and a separate MySQL service for testing:
Below are the environment variable settings in the .env
file from the aforementioned blog post:
DB_DATABASE=myapp
DB_USERNAME=myapp
DB_PASSWORD=myapp
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=8001
The RefreshDatabase trait and parallel tests
In Laravel tests we use the RefreshDatabase
trait to truncate the test database tables and apply database migrations before running tests.
When connecting to in memory databases this happens before each test. However when running against a database such as MySQL with transactional capabilities, the trait only applies database migrations one time before running the first test and uses transactions to roll back any changes made to the database between subsequent tests.
The way the RefreshDatabase
trait achieves this under the hood is by first, applying the RunsMigrations
trait which is responsible for running the database migrations only once before any tests are run. Second, by applying the UsesTransactions
trait which starts a database transaction before running each test and rolls back the transaction after the test.
By rolling back the transaction, the database state goes back to the state right after applying the migrations and we don’t have to truncate the tables and run the migration again before each test which reduces performance.
When tests are run serially using the RefreshDatabase
trait works without issues. However when test are run in parallel one or more tests will try to run the code associated with the RunsMigrations
trait at the same time. This will cause migrations being applied simultaneously by multiple tests, even though migrations only run once at the start of testing.
The solution to this problem is to run the migrations just once manually before running phpunit tests. Then we can use the UsesTransactions
trait instead of the RefreshDatabase
trait in our tests so the tests do not run migrations but still rollback changes made during each test.
Since RefreshDatabase
runs UsesTransactions
after applying migrations, we are still doing exactly the same thing.
Since we won’t be running the database migrations using the RunsMigrations
trait anymore, therefore overriding the DB_CONNECTION
environment setting to allow the database migrations to be applied to the test database by RunsMigrations
trait will not work.
Setting up the .env.testing file
In order to tell the artisan database migration command to run migrations against the test database server we must setup a new environment variable file for testing. The name of this file must be .env.testing
so that by default phpunit will use it instead of the .env
file. This way both the migration command and phpunit will be using the same test database settings.
In the .env.testing file we need to change the DB_PORT environment variable value to the mapped port number of the test database running in the docker container.
So first we need to copy the .env
file to .env.testing
file in the root directory of the Laravel project.
Then in the .env.testing
file we can change the DB_PORT
setting value from 8001 to 8011 to connect to the test database server port running in docker.
Running migrations with Artisan using .env.testing
Now we need to pass the .env.testing
file name to the artisan db:migrate
command so that the database migrations will use the DB_PORT
setting from the .env.testing
file.
To allow the php artisan migrate command to use the .env.testing
file we must pass it as a option flag to the command like so:
php artisan migrate --env=testing
Setting up phpunit to automatically use DatabaseTransactions trait
For reasons mentioned above we will use the DatabaseTransactions
trait instead of the RefreshDatabase
in our unit tests.
So that we don’t have to type use DatabaseTransactions
in all our test classes, we can extend the base BaseTestCase
class and add the trait there.
Then our test classes can extend the base Tests\TestCase
class to take advantage of the DatabaseTransactions
trait in all classes derived from that extended class.
namespace Tests\Feature;
use Tests\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
abstract class TestCase extends BaseTestCase
{
use DatabaseTransactions;
}
So now all our classes will need to be derived from the Tests\Feature\TestCase
class.
Using Paratests to run phpunit tests in parallel
To install the parallel testing package run:
composer require --dev brianium/paratest
To execute tests using paratest run:
paratest --processes 4 --testsuite Feature --runner WrapperRunner
Automating our test setup
In order to automate running the migration before running the tests in a single command we can create a composer script to run the commands in sequence:
{
"scripts": {
"ft": [
"php artisan migrate --env=testing",
"./vendor/bin/paratest --processes 4 --runner WrapperRunner --testsuite Feature"
],
"ut": [
"./vendor/bin/paratest --processes 4 --runner WrapperRunner --testsuite Unit"
]
}
}
I have also added a setting for running unit tests with a short command.
Now all we have to do to run our feature tests in parallel is to type
composer ft
on the command line.
This way you make sure that the migrations are run for feature tests before running the rests and make the command to run the tests is short and easy to type as well.
Alternatively you could add the following aliases in your bash profile:
ft="php artisan migrate --env=testing && ./vendor/bin/paratest --processes 4 --runner WrapperRunner --testsuite Feature"
ut=" ./vendor/bin/paratest --processes 4 --runner WrapperRunner --testsuite Unit"
Then simply type ft
or ut
while in the root directory of your project to run the feature or unit tests.