If you followed my previous post on Integration Testing With Docker-Compose you may have gone the next step and tried to implement this within your CI pipeline.
I use GitLab and usually set up my system the same as the gitlab.com system,
where jobs are run in Docker containers. When GitLab is running jobs in Docker containers
and those jobs run
docker-compose to instantiate Docker containers: it all gets “a bit Inception”.
So let’s look at how it all works.
First up, we need a container with both
docker-compose installed within it from which
we can run our GitLab job.
I did some searching, but didn’t find exactly what I needed, so I created one.
Dockerfile is pretty simple, using the base
docker image and adding
docker-compose like so:
FROM docker:18.06.1-ce RUN apk add --no-cache py-pip && pip install --upgrade pip && pip install docker-compose
You can simply use the pre-built image from Docker Hub.
Then we’re going to need a GitLab CI YAML file that tells GitLab how to run our integration test.
The code below is a really simple CI file that uses the
docker + docker-compose image from above.
services: - docker:dind service-under-test: image: service-under-test integration-tests: image: davedupplaw/docker-compose-ci:latest stage: test script: - docker-compose build - docker-compose up --abort-on-container-exit after_script: - docker-compose down --rmi 'all' || true
You can see we’re using the
docker + docker-compose image as the build image; that means our build will run within
this container. Then to run the tests, we build any containers from our docker-compose file (usually the
test image) and spin up the stack using ``–abort-on-container-exit` so that the stack is pulled down
when the test finishes.
Now, one thing to be aware of is how this manifests itself within the GitLab environment. GitLab runners that use Docker link to the underlying Docker machine on the host on which it sits. That means that the containers that are spun up from the compose stack as spun up as siblings to the build container.
This is the reason we also have an
after_script section which removes all the images that we pulled during
this build. If you’re reusing the same host machine for builds, then if
service-under-test:latest image already
exists, it will not force pull a new one. This means you can end up running integration tests against old
versions of your services, so we avoid that by removing all images after our test. It also ensures our
host machine will not fill up with images.
‘Where are my test reports?’, I hear you say.
They’re in the container in which your tests ran!
‘But that container shut down at the end of the tests’
Oh yes, it did. But you can still retrieve the reports by copying data out of the shutdown container.
However, you need to make sure that you do it in the
after_script so that if the test fails, the
tests reports will still get copied out. You also need to do it before you remove all the containers
services: - docker:dind service-under-test: image: service-under-test integration-tests: image: davedupplaw/docker-compose-ci:latest stage: test script: - docker-compose build - docker-compose up --abort-on-container-exit after_script: - docker cp integration-tests:/path/to/repots ./reports - docker-compose down --rmi 'all' || true artifacts: when: always path: - reports
So now the first line in the
after_script copies the reports from our test container into the
build container. This is then exposed to GitLab as an artifact via the
We use the
when: always declaration to ensure that our artifacts are exposed even if the job fails.
For a real example, here’s a job we have to run integration tests against our UI, using Cypress.
ui-tests: image: davedupplaw/docker-compose-ci:latest stage: test script: - docker login -u $DOCKER_USER -p $DOCKER_PASSWORD <our-private-repo> - docker-compose build - docker-compose up --abort-on-container-exit after_script: - docker cp ui-test:/usr/src/myapp/cypress/screenshots ./cypress/screenshots - docker cp ui-test:/usr/src/myapp/cypress/videos ./cypress/videos - docker-compose down --rmi 'all' || true artifacts: expire_in: 1 week when: always paths: - cypress/screenshots - cypress/videos
I hope this was useful to you. If so, let me know in the comments below!