3 min read

Docker: MySQL for Laravel Testing with Devcontainer

I decided to switch from SQLite to MySQL for Laravel testing. You can read about the reasons here.

I always use Docker and VSCode’s devcontainer for my development environment. Therefore, I needed to update my Docker Compose file and add a database service specifically for when running pest.

For running pest, I created a separate service and run it with docker compose run --rm -it pest.

I also grouped it with cli as a profile so that it does not run automatically when bringing up the entire container using docker compose up -d.

docker-compose.yml
pest:
  image: serversideup/php:8.1-cli
  working_dir: /var/www/html
  volumes:
    - .:/var/www/html/:cached
  environment:
    - PUID=${UID:-1000}
    - PGID=${GID:-1000}
  entrypoint: ./vendor/bin/pest
  profiles:
    - cli

Now, let’s add the database service for testing only.

docker-compose.yml
pest:
  image: serversideup/php:8.1-cli
  working_dir: /var/www/html
  volumes:
    - .:/var/www/html/:cached
  environment:
    - PUID=${UID:-1000}
    - PGID=${GID:-1000}
  entrypoint: ./vendor/bin/pest
  profiles:
    - cli
  depends_on:
    - mariadb-testing
 
mariadb-testing:
  image: mariadb:10.11
  tmpfs: /var/lib/mysql
  environment:
    MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
    MARIADB_DATABASE: testing
  healthcheck:
    test: ["CMD", "mysqladmin", "ping"]
    interval: 10s
    timeout: 5s
    retries: 3
  profiles:
    - cli

We need to ensure that pest only runs after mariadb-testing is up, which is why we added the depends rule for the pest service.

Before running it, let’s also adjust phpunit.xml.

phpunit.xml
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="DB_CONNECTION" value="mysql"/>
<env name="DB_HOST" value="mariadb-testing"/>
<env name="DB_DATABASE" value="testing"/>
<env name="DB_USERNAME" value="root"/>

Now, let’s run it, and… we encounter an error.

SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for mariadb-testing failed: Temporary failure in name resolution (Connection: mysql, SQL: SHOW FULL TABLES WHERE table_type = 'BASE TABLE')
SQLSTATE[HY000] [2002] Connection refused (Connection: mysql, SQL: SHOW FULL TABLES WHERE table_type = 'BASE TABLE')

Even though the database service is already running, it’s not ready for the connection. However, after a few retries, the test passes.

Therefore, the healthcheck in mariadb-testing is not effective, as it gives a false positive result.

The solution is to add another service using the busybox image, which essentially waits for the database to be fully ready for connection.

docker-compose.yml
pest:
  image: serversideup/php:8.1-cli
  working_dir: /var/www/html
  volumes:
    - .:/var/www/html/:cached
  environment:
    - PUID=${UID:-1000}
    - PGID=${GID:-1000}
  entrypoint: ./vendor/bin/pest
  profiles:
    - cli
  depends_on:
    wait-for-mariadb-testing:
      condition: service_completed_successfully
 
mariadb-testing:
  image: mariadb:10.11
  tmpfs: /var/lib/mysql
  environment:
    MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
    MARIADB_DATABASE: testing
  profiles:
    - cli
 
wait-for-mariadb-testing:
  image: busybox:latest
  depends_on:
    - mariadb-testing
  command:
    [
      "sh",
      "-c",
      "until nc -vz mariadb-testing 3306 ; do echo 'waiting for mariadb-testing:3306' ; done",
    ]
  profiles:
    - cli

Now, when running the pest service, the testing will wait until the database is fully ready 🎉.

Tips

I’ve created some bash aliases to help me run docker compose and docker compose run.

.bashrc
dc() {
  docker compose "$@"
}
 
 
dr() {
  docker compose run --rm -it -u $(id -u):$(id -g) "$@"
}

Now, I just need to run these from my project folder.

dc up -d # up all services
dc down # down all services
 
dr pest # run specific service
dr artisan # another service