How to run E2E Tests with docker-compose

This guide covers using docker-compose to spin up your application, run E2E tests, and then exit with the results.

This post is a good companion to my post NestJS Integration and E2E Tests with TypeORM, PostGres, and JWT.

The TL;DR is:

docker-compose -f docker-compose.e2e.yml up --abort-on-container-exit --exit-code-from app

For the sake of an example, we’ll be testing a hypothetical API written in NodeJS that uses a Postgres database and a Redis instance. We’ll assume tests are run via jest. You can substitute any stack!

To begin, ensure that you have recent versions of docker and docker-compose installed on your machine.

Dockerfile

Start by creating a Dockerfile in your project folder that specifies how to build an image for your app.

The following example is for a NodeJS web API.

FROM node:14.4-alpine As example

# install build dependencies
RUN apk update && apk upgrade
RUN apk add python3 g++ make

# install packages for sending mail (msmtp = sendmail for alpine)
RUN apk add msmtp
RUN ln -sf /usr/bin/msmtp /usr/sbin/sendmail

# make target directory for assigning permissions
RUN mkdir -p /usr/src/app/node_modules
RUN chown -R node:node /usr/src/app

# use target directory
WORKDIR /usr/src/app

# set user
USER node

# copy package*.json separately to prevent re-running npm install with every code change
COPY --chown=node:node package*.json ./
RUN npm install

# copy the project code (e.g. consider: --only=production)
COPY --chown=node:node . .

# expose port 3500
EXPOSE 3500

docker-compose

Create a docker-compose.e2e.yml file.

The following example creates a service called app that runs in the container example, as specified in the Dockerfile.

The command property should specify the command that will run your tests inside the container. In our example, this is yarn test:e2e.

version: '3.8'

services:
  app:
    container_name: example
    build:
      context: .
      target: example # only build this part of the Dockerfile (see: '... As example' )
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules # 'hack' prevents node_modules/ in the container from being overridden
    working_dir: /usr/src/app
    command: yarn test:e2e
    environment:
      PORT: 3500
      NODE_ENV: test
      DB_HOSTNAME: postgres
      DB_PORT: 5432
      DB_NAME: example
      DB_USERNAME: postgres
      DB_PASSWORD: postgres
      REDIS_HOSTNAME: redis
      REDIS_PORT: 6379
    networks:
      - webnet
    depends_on:
      - redis
      - postgres

  redis:
    container_name: redis
    image: redis:5
    networks:
      - webnet

  postgres:
    container_name: postgres
    image: postgres:12
    networks:
      - webnet
    environment:
      POSTGRES_DB: example
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      PG_DATA: /var/lib/postgresql/data
    volumes:
      # - ./seed.db.sql:/docker-entrypoint-initdb.d/db.sql <- run only once when the pgdata volume is first created (when run via docker-compose)
      - pgdata:/var/lib/postgresql/data # or specify a local folder like ./docker-volumes/pgdata:/var/lib/postgresql/data

networks:
  webnet:

volumes:
  pgdata:
  logs:

Note how each service shares the same network (called webnet) so they can communicate with each other.

Tip: you can use .env files and reference variables from them in a docker-compose.yml file as follows: ${VARIABLE_NAME}.

If you wish to specify a particular .env file in your docker-compose.yml file:

env_file:
  - .env

Run E2E Tests

From your project folder, you can run the following command to run your tests:

docker-compose -f docker-compose.e2e.yml up --abort-on-container-exit --exit-code-from app

The -f flag specifies a custom configuration file for docker-compose. If this is not specified, docker-compose will look for docker-compose.yml by default.

The up command tells docker-compose to bring the containers and services up.

The --abort-on-container-exit and --exit-code-from flags are an important combination.

The first flag shuts things down when our test run is complete, and the second flag will use the exit code from the specified service (in our case the one named app) as the exit code from the overall docker-compose command.

This is a good setup if you have scripts that run tests, or if you have a continuous integration pipeline that automatically runs tests and requires a pass/fail.

Test runners such as jest will generally exit with code 0 (success) if all tests pass, and exit with a non-zero code (failure) if any tests fail.

package.json

If your project uses npm, yarn or similar, you can specify commands to run tests in the scripts section.

Our docker-compose.e2e.yml file requires the app service to run the command: yarn test:e2e. For our hypothetical example app, this is specified as follows:

"test:e2e": "NODE_ENV=test jest --config ./test/jest-e2e.json",

Of course, you will need to specify whatever command initiates running E2E tests that’s particular to your project and environment.

To spin up your application via docker-compose and run tests from your workstation (or CI solution, etc), add the following script:

"test:e2e:docker": "docker-compose -f docker-compose.e2e.yml up --abort-on-container-exit --exit-code-from app"

You can then run via npm run test:e2e:docker or yarn test:e2e:docker to spin up your test environment, run your tests, and exit with the results.

If you are using a different package/dependency management solution, you can specify your test-related scripts there. You also have the option to define shell/bash scripts that can run your tests.