unsplash-logoNeven Krcmarek

Recently, I’ve worked on setting up testing for a mono-repo at Assignar.
We use jest for testing, and although jest added a [multi-project runner] over a year ago, I couldn’t find any guides on it which made the implementation a bit of a pain.

This post is about my experience with the implementation.

The Problem.

Monorepos are a source control pattern where all of the source code is kept in a single repository. This pattern has it's challenges. For testing, they include:

  • Management. Testing can become unmanageable in a monorepo as there are many smaller projects that have their own setup and configuration.
  • Running tests. Having to cd into every directory and run tests for each project is time consuming, and you miss out on unified coverage report.

The solution

Jest projects helps you manage and run tests for project in a mono-repo using a single instance of jest.

Goals

  • Run tests globally from the root.
  • Run tests from individual project directories.
  • Let developers manage their own jest, babel, and typescript configurations, i.e. shared common configuration in the root, and have custom configuration in each package.

Project Structure

 ├──assignar   
 │   ├── packages
 │   │   ├── packageA
 │   │   │   ├── jest.config.js - jest config file for packageA (extends base.config.base.js)
 │   │   │   ├── .babelrc - used by `babel-jest` to transpile ts in packageA
 │   │   ├── packageB
 │   │   │   ├── jest.config.js - jest config file for packageB (extends base.config.base.js)
 │   │   │   ├── .babelrc - used by `babel-jest` to transpile ts in packageA
 │   │   │── packageC
 │   │   │   ├── jest.config.js - jest config file for packageC (extends base.config.base.js)
 │   │   │   ├── .babelrc - used by `babel-jest` to transpile ts in packageA
 │   ├── jest.config.base.js - (base jest config, imported by other packages)
 │   ├── babel.config.js - (used to instruct babel to use `.babelrc` files within the each package root directory`
 └── └── jest.config.js - config used to configure projects and run all tests 

Root Configuration

The root of the app contains:

  • jest.config.js
  • jest.config.base.js
  • babel.config.js

jest.config.js
To setup jest projects for the directory structure above, I specify a path glob.

// jest.config.js
module.exports = {
...,
projects: [ '<rootDir>/packages/*/jest.config.js']
}

I end up with a file that looks like this.

const baseConfig = require('./jest.config.base')

module.exports = {
    ...baseConfig,
    projects: [
            '<rootDir>/packages/*/jest.config.js',
    ],
    coverageDirectory: '<rootDir>/coverage/',
    collectCoverageFrom: [
        '<rootDir>/packages/*/src/**/*.{ts,tsx}',
    ],
    testURL: 'http://localhost/',
    moduleNameMapper: {
        '.json$': 'identity-obj-proxy',
    },
    moduleDirectories: [
        'node_modules',
    ],
    snapshotSerializers: [
        'enzyme-to-json/serializer',
    ],
}

jest.config.base.js
The base config ./jest.config.base.js contains configuration that's shared across the monorepo.

babel.config.js
We use babel-jest for ts transpilation, and so I had to figure out a way to let babel use the babelrc files in individual project directories. I was shocked to find that this doesn't work out of the box.
After a bit of digging, I found this comment on github that explains that you have to specify babelRcRoots: "packages/*" in babel.config.js(which is a new non-heirarchical config file that loads from the root directory) to instruct babel to use the configurations in subdirectories.
There's more information on that config file in the babel docs

Project Configuration

Based on my monorepo's jest.config.js configuration above, jest looks for jest.config.js files in my packages directory and sets up the parent directories of those files as projects.

Root Directory
I set the root directory of all project configurations as the root directory of the mono-repo.i.e. {rootDir: '../..'}.

As a result I have to specify absolute path all over my configuration instead of relative path. e.g.

 {
 setupTestFrameworkScriptFile:rootDir>/packages/${packageName}/jest/setupJest.ts
 }

Naming
Jest provides a displayName option, which is shown nexts to the tests in the terminal.

Module Paths
I ran into ts path issues while running the tests. Some typescript project configurations specify a baseUrl of ./src, and typescript tries to resolve modules using monorepoRootDir/src.

Jest has a modulePaths option to let you provide an array of absolute paths to search when resolving modules. This solved the above issue.

Each package ends up with a configuration like this.

'use strict'
const baseConfig = require('../../jest.config.base')

const packageName = require('./package.json').name.split('@assignar/').pop()

module.exports = {
    ...baseConfig,
    roots: [
        `<rootDir>/packages/${packageName}`,
    ],
    collectCoverageFrom: [
        'src/**/*.{ts,tsx}',
    ],
    setupFiles: [
        `<rootDir>/packages/${packageName}/jest/polyfills.ts`,
        `<rootDir>/packages/${packageName}/jest/setupEnzyme.ts`,
    ],
    setupTestFrameworkScriptFile: `<rootDir>/packages/${packageName}/jest/setupJest.ts`,
    testRegex: `(packages/${packageName}/.*/__tests__/.*|\\.(test|spec))\\.tsx?$`,
    testURL: 'http://localhost/',
    moduleNameMapper: {
        '.json$': 'identity-obj-proxy',
        'lodash-es': 'lodash',
    },
    moduleDirectories: [
        'node_modules',
    ],
    modulePaths: [
        `<rootDir>/packages/${packageName}/src/`,
    ],
    snapshotSerializers: [
        'enzyme-to-json/serializer',
    ],
    name: packageName,
    displayName: packageName,
    rootDir: '../..',
}

Alternatives

Lerna is a tool that optimizes the workflow around managing multi-package repositories.
Lerna has a test command you can run. lerna run test runs a script that goes through each package and runs the test script declared in package.json. This is very slow, might lose syntax highlighting, and use up too much resources as it spawns tests in seperate processes. Also it doesn't solve any of the problems we faced, so it wasn't a viable option.


This has worked out well, as we now able to run pre-commit hooks using lerna's --onlyChanged option, and we have one coverage report for all projects across the mono-repo.

One challenge is that I've been unable to use jest's --findRelatedTests option, which would be greate for CI.
I hope to figure that out soon.

One last thing

If you've found this post helpful, consider supporting the blog using the button below.