As part of my development workflow, I usually run some integration tests. An integration test means that code interacts with some outside dependency. This could be a temporary Postgres server in a Docker container, or it could be an HTTP server. These dependencies usually take some time to start. In particular, enough time to start that our tests would fail if we ran them immediately after starting our dependencies.
To make this concrete, let’s use docker-compose to start a Postgres server in a Docker container.
Our docker-compose.yml
becomes
version: "3.8"
services:
postgres:
image: postgres:13-alpine
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: admin
POSTGRES_USER: admin
and we can start it by doing docker-compose up
.
We also want a Makefile
to perform the whole test, including the setup.
The structure of our Makefile
is
- start Docker containers
- wait until dependencies are ready
- execute tests
- shut down Docker containers
The lazy way to do step 2 is to wait a set amount of time before executing tests, typically by running the shell command sleep N
.
To be sure that the dependencies are indeed ready when we start our tests, we will want to set N
to a safe upper bound on the depency’s startup time.
If we run locally, we probably have the Docker containers cached, so docker-compose up
might not take long.
However, if we run our CI in Github Actions, the containers will not be cached, and so the wait time is longer and more unpredictable.
This means that if we set the time to a comfortable upper bound so we sleep long enough in CI, we risk wasting time each time we run our tests locally.
But there are better solutions!
until
The until
shell command will run a command until it succeeds.
Like a while
loop but with the predicate switched.
If we find a shell command that returns exit code 0 (indicating success) if the dependencies are ready, and not 0 if they are not ready, we can put this inside an until
loop.
It turns out that the Postgres client psql
comes with a command called pg_isready
, which will do just that:
It pings a Postgres server and returns a non-zero exit code if it’s not ready.
The complete Makefile becomes
all:
make test
test:
docker-compose up -V -d postgres
until pg_isready --host localhost --port 5432; do \
sleep 0.1; \
done
echo "my test here"
docker-compose down
Try it yourself by cloning this Github repo and running make
!
Bonus: HTTP server
If we want to wait for an HTTP server to be responsive, we can instead run this curl
command:
curl --output /dev/null --silent --head --fail http://localhost:${PORT}
which becomes the following in a Makefile:
until $$(curl --output /dev/null --silent --head --fail "http://localhost:8080"); do \
sleep 0.1; \
done