Testing Microservices with Cypress, Docker & CircleCI

Quinton Aiken
7 min readAug 2, 2019

--

Photo by Guillaume Bolduc on Unsplash

July 2023 Update: I just released an interactive course on web application security — check it out on educative.io!

In the past, integration/E2E tests have been hard to orchestrate and getting different applications coordinating with each-other in CI has been non-obvious. However, with the meteoric rise of microservices and containerization, it’s no longer an option to skip this layer of testing if we want to have the most confidence in our applications. You can read more about the differences between unit, integration and E2E testing here. Fortunately, as Cypress points out:

“The web has evolved. Finally testing has too.”

Cypress + Docker + CI = ❤️. Cypress and Docker allow you to completely decouple the testing framework from the actual application code. This setup can essentially test any web app regardless of your programming language or application framework of choice.

This tutorial will focus on setting up the infrastructure in order to run E2E tests across multiple repos locally and in CI — not on writing the actual tests.

This article is highly influenced by these two great articles and I recommend you read them as well:

Demo App

The example app code can be found here and the example API code can be found here. I recommend cloning the repos and following along.

The actual application we will be testing is very silly and simple:

WOW

When users land on our app, they will see a list of items pulled from an API. They can click a button to remove all those items. Afterwards, they should see something like this:

none

The frontend is using create react app (let’s pretend this is an app that justifies using it) and our Dockerfile is simple:

FROM node:8# Create app directory
WORKDIR /app
COPY package.json yarn.lock ./RUN yarn# Bundle app source
COPY . .
EXPOSE 3000CMD [ "npm", "start" ]

In reality, this is a Dockerfile for local development because it is using webpack-dev-server. For more information on dockerizing node apps check out this great guide. If you’re interested in serving up a create-react-app instance in production, check this reference out.

The node API lives in a separate repo but its Dockerfile is equally simple:

FROM node:8# Create app directory
WORKDIR /api
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./
RUN npm install# Bundle app source
COPY . .
EXPOSE 8080CMD [ "npm", "start" ]

Now we need to get these apps talking! The docker-compose.yml file within our app directory looks like this:

version: '3.2'
services:
app:
build:
context: .
ports:
- 3000:3000
env_file:
- .env
volumes:
# share everything BUT node_modules from the host
- .:/app
- /app/node_modules
depends_on:
- api
api:
build:
context: ../cypress-demo-api
environment:
- PORT=8080
ports:
- 8080:8080
volumes:
# share everything BUT node_modules from the host
- ../cypress-demo-api:/api
- /api/node_modules

This is assuming that if we move up one directory from our app code, our API code will live in a directory called cypress-demo-api.

Let’s abstract our docker config with a Makefile!

.PHONY: dev cleanDEV_DC = docker-composedev:
$(DEV_DC) up --build
clean:
$(DEV_DC) stop
$(DEV_DC) rm -f
  • Now we can run make dev and navigate to http://localhost:3000 and our app should be running! Sweet!
  • We can also run make clean to stop and remove all our containers which is handy if we want to reset everything.

Running E2E Tests

So where should we put the E2E tests? Where should Cypress live? If you look at the app’s package.json file, you will notice that Cypress is no where to be found. However, there is a separate top-level directory titled e2e which contains our Cypress config and tests.

We can create a separate docker-compose.cypress.yml file to kick off our Cypress instance:

version: '3.2'
services:
app:
environment:
- REACT_APP_API_URL=http://api:8080
cypress:
image: 'cypress/included:3.2.0'
depends_on:
- app
entrypoint: /scripts/wait_for_it.sh app:3000 -- cypress run
environment:
- CYPRESS_baseUrl=http://app:3000
working_dir: /e2e
volumes:
- ./e2e:/e2e
- ./scripts:/scripts

Using multiple Compose files enables you to customize your config for different environments and workflows. You can read more about it here.

  • We redefine a service called app and set an environment variable called REACT_APP_API_URL to http://api:8080. We can use the container name (api) as a hostname. In our docker-compose.yml file, we had previously used a .env file to set REACT_APP_API_URL to http://localhost:8080 but now we want our apps to communicate within dockerland.
  • We define a new service called cypress using an official cypress docker image.
  • Our entrypoint instruction is cypress run and we use the wait_for_it shell script to test and wait on the availability of our app host and port.
  • Our CYPRESS_baseUrl tells Cypress where it can access our app and we can again use the container name (app) as a hostname.

We then need to tweak our Makefile so that we can use our new docker-compose file:

.PHONY: dev e2e cleanDEV_DC = docker-composedev:
$(DEV_DC) up --build
E2E_DC = docker-compose -f docker-compose.yml -f docker-compose.cypress.yml -p e2ee2e:
$(E2E_DC) build
$(E2E_DC) up --exit-code-from cypress
clean:
$(DEV_DC) stop
$(DEV_DC) rm -f
$(E2E_DC) stop
$(E2E_DC) rm -f
  • We use -f to specify and compose multiple docker-compose files together.
  • We use -p to set an alternate project name. The current directory name is used by default and we want to give some context to our containers.
  • We use --exit-code-from so that all our containers stop when our cypress service exits.

We can now run make e2e and our cypress tests will run!

tests!

Running Interactive E2E Tests

But wait! One of the best features of Cypress is the interactive debugger! Do we lose the ability to use it when running Cypress in dockerland? Fear not. Using Gleb Bahmutov’s awesome ideas as described in “Run Cypress with a single Docker command”, let’s create another docker-compose file titled docker-compose.cypress-interactive.yml :

version: '3.2'
services:
cypress:
entrypoint: /scripts/wait_for_it.sh app:3000 -- cypress open --project /e2e
environment:
- DISPLAY
volumes:
- /tmp/.X11-unix:/tmp/.X11-unix
  • We redefine our cypress service but this time use cypress open instead of cypress run to open the Cypress Test Runner in interactive mode.
  • We use --project /e2e to tell cypress where our cypress.json file is located. By default, Cypress expects your cypress.json file to be found where your package.json file is.
  • In order to see Cypress in interactive mode via dockerland, you need to forward the XVFB messages from Cypress out of the Docker container into an X11 server running on the host machine. In order for Cypress to communicate with the X11 server, we pass the socket file: /tmp/.X11-unix:/tmp/.X11-unix
  • We also need to pass an X11 DISPLAY environment variable. Where do we set this? The Makefile!
IP        ?= $(shell ipconfig getifaddr en0)
DISPLAY := $(IP):0
export.PHONY: dev e2e e2e-interactive cleanDEV_DC = docker-composedev:
$(DEV_DC) up --build
E2E_DC = docker-compose -f docker-compose.yml -f docker-compose.cypress.yml -p e2ee2e:
$(E2E_DC) build
$(E2E_DC) up --exit-code-from cypress
E2E_INTERACTIVE_DC = docker-compose -f docker-compose.yml -f docker-compose.cypress.yml -f docker-compose.cypress-interactive.yml -p e2e-interactivee2e-interactive:
open -a XQuartz
xhost + $(IP)
$(E2E_INTERACTIVE_DC) build
$(E2E_INTERACTIVE_DC) up --exit-code-from cypress
clean:
$(DEV_DC) stop
$(DEV_DC) rm -f
$(E2E_DC) stop
$(E2E_DC) rm -f
$(E2E_INTERACTIVE_DC) stop
$(E2E_INTERACTIVE_DC) rm -f
  • We get the IP of the host machine and set it to an environment variable called IP. We use it to create the X11 DISPLAY environment variable which is used by our cypress service in interactive mode. We export the environment variables to the subshells used by the Makefile commands.
  • We define a new e2e-interactive command which opens XQuartz and adds the host IP to the allowed X11 hosts before running our containers with the new docker-compose.cypress-interactive.yml file.

Now we just run make e2e-interactive and bam! We can debug our E2E tests!

running inside dockerland!

Running in CI

Because of our work containerizing Cypress and abstracting how we run our tests, getting them to run in CI is stupid simple. Here is the .circleci/config.yml file:

version: 2
jobs:
test:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
git clone git@github.com:qaiken/cypress-demo-api.git ../cypress-demo-api
- run:
command: |
make e2e
workflows:
version: 2
test:
jobs:
- test:
filters:
branches:
only:
- master
  • We use the machine executor type since we are building Docker images during our workflows. We also enable docker_layer_caching which caches the individual layers of our Docker images built during each job so that unchanged image layers are reused on subsequent CircleCI runs, rather than rebuilding the entire image every time. Read more about it here.
  • We then clone our api code, from master, into a directory next to our app code and simply run make e2e.

Testing never felt so good!

P.S. — I also released a course on mastering technical interviews. Check it out!

--

--