How to use Docker and Jest without compromises

Nov 3, 2018 18:02 · 620 words · 3 minute read docker typescript nodejs jest

Previously I wrote about how my team worked with TypeScript and Docker on a recent project. Today, I’ll be elaborating on that with how we added Jest to the mix.

Jest is a great library for testing JavaScript, and with ts-jest its a great library for testing TypeScript too. If we just wanted to test our TypeScript without external connections, running Jest directly on our dev machines would have been fine. However, we also wanted to test the API’s interaction with the database, meaning that we also needed Docker to work alongisde Jest. More importantly, we wanted to be able to use its CLI with its full set of options, but without needing to fight Docker when doing so.

At first we tried the same approach we used before, using our Compose file to override the command with yarn test (after installing dependencies):

version: '3.4'
services:
    server:
        image: node:10.10.0-alpine
        command: sh -c "yarn --no-progress && yarn test"

However this was very rigid, giving us no way to use any of Jest’s CLI options.

The key was to make use of how yarn run directly passes additional arguments to a script.

We were already using a package.json in our project’s root directory for easy starting of our server for dev:

// package.json scripts
{
    "start:prod": "docker-compose -f docker-compose.yml -f docker-compose.prod.yml up",
    "start": "docker-compose up"
}

So instead of providing the command in our test Compose file, we can provide it as part of a script:

// package.json scripts
{
    "test:install": "docker-compose -f docker-compose.yml -f docker-compose.test.yml run --no-deps test yarn --no-progress",
    "test": "yarn test:install && docker-compose -f docker-compose.yml -f docker-compose.test.yml run test yarn test"
}

There are a few things going on here, so lets break it down.

We have a separate test:install script which installs the package dependencies in the test service without starting the postgres service that it depends on. After that, we have docker-compose -f docker-compose.yml -f docker-compose.test.yml run test yarn test:

  • -f docker-compose.yml -f docker-compose.test.yml specifies the Compose files we are using.
  • run test specifies we are running the test service
  • yarn test specifies that the command the service should run is yarn test (which was defined in server/package.json as jest).

Put another way, we have docker-compose run with arguments to specify Compose files (-f docker-compose.yml -f docker-compose.test.yml), an argument to specify the service to run (test, whose definition comes from the Compose files), and an argument to specify the command to be run by the service (yarn test).

This lets us pass any argument to the yarn test script in in our root directory, and it will be forwarded to the service’s yarn test. For example, yarn test --watch is equivalent to running yarn test:install && docker-compose -f docker-compose.yml -f docker-compose.test.yml run test yarn test --watch, which has the service run yarn test --watch i.e. jest --watch. And yes, there’s a lot of indirection here, but it’s worth it to make the dev experience quick and easy.

We also had to make one final tweak to our test Compose file. We were using a monorepo to ensure the API and webapp could not get out of sync (as an aside, here is a great article about monorepos), so to have Jest know about file changes for jest -o or jest --watch we also needed to mount the root directory so it could see the .git directory, rather than just the server directory as we did previously. An abbreviated directory structure may make this clearer:

project
├─── package.json
├─── .git
├─── server
     ├─── package.json

Hopefully these articles helped demonstrate how Docker and Docker Compose can fit in your workflow. Docker is a great tool, and although it has a learning curve it deserves a place in your toolbox.