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 and 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.
The 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.

Docker Compose Container on GitLab

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 and images.

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 artifacts declaration. 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!