Monorepos By Example: Part 2

John Tucker
codeburst
Published in
4 min readMar 2, 2018

--

After a quick sidebar into dependency duplication, we explore the testing benefits of monorepos.

This article is part of a series starting with Monorepos By Example: Part 1. The final monorepo for this series is available for download.

Dependency Duplication

I bumbled into an interesting issue in in the previous article; thought to illuminate.

As a reminder, we ended with the following folder structure and dependencies:

  • apple and banana depends on sillyname@0.0.3
  • grocery depends on sillyname@0.1.0
  • grocery depends on apple and banana

Observations:

  • When we run index.js in grocery, the code in grocery use sillyname@0.1.0 and the code in apple and banana use sillyname@0.0.3 (during execution we have two versions of sillyname in use).
  • In this particular case, there are no problems because the functionality of sillyname is pure, specifically; sillyname has no side-effects.
  • At the same time, if we were building a frontend application, we would be storing two versions of sillyname in the bundle. As bundle size is an important constraint, this would be a problem.

For a more theoretical discussion of the issue, I would recommend:

Will revisit this issue in a later article in this series; specifically as it relates to using a frontend design library dependency that is impure (solution is to use peer dependencies).

Testing

One of the arguments of using a monorepo is that it is easier to test; let us explore this using Jest.

note: This article assumes a basic understanding of Jest; I wrote another article on testing that you might find helpful; Revisiting Node.js Testing: Part 1.

While we normally would run Jest from each of the packages, we will follow the advice:

The root package.json, at the very least, is how you install lerna locally during a CI build. You should also put there your testing, linting and similar tasks there to run them from root as running them separately from each package is slower. The root can also hold all the “hoisted” packages, which speeds up bootstrapping when using the — hoist flag.

Lerna — FAQ

First we install Jest into the root:

npm install jest --only=dev

And create and update:

jest.config.js

module.exports = {
collectCoverage: true,
collectCoverageFrom: [
'packages/**/*.{js}',
'!**/node_modules/**',
],
roots: [
'packages/',
],
};

package.json

{
"scripts": {
"test": "jest"
},

"devDependencies": {
"jest": "^22.4.2",
"lerna": "^2.9.0"
}
}

Next, the monorepo has no benefits for doing unit testing; e.g., we write a mock for sillyname and the test for the apple (likewise for banana) module as we usually would:

packages/(apple|banana)/__mocks__/sillyname.js

module.exports = () => 'TEST';

packages/(apple|banana)/index.test.js

const apple = require('./');test('returns correct string', () => {
expect(apple).toBe('apple and TEST');
});

Likewise the mocks (all the same) for apple, banana, and sillyname and test for the grocery module.

packages/grocery/__mocks__/(apple|banana|sillyname).js

module.exports = 'TEST';

packages/grocery/index.test.js

const log = jest.fn();
console.log = log;
const grocery = require('./');
test('outputs correct string', () => {
expect(log.mock.calls.length).toBe(3);
expect(log.mock.calls[0][0]).toBe('grocery and TEST');
expect(log.mock.calls[1][0]).toBe('TEST');
expect(log.mock.calls[2][0]).toBe('TEST');
});

But the monorepo does helps us write integration tests, e.g., say we wanted to test the integration of apple and banana with grocery.

integration.test.js

jest.unmock('apple');
jest.unmock('banana');
const log = jest.fn();
console.log = log;
const grocery = require('./packages/grocery/');
test('outputs correct string', () => {
expect(log.mock.calls.length).toBe(3);
expect(log.mock.calls[0][0]).toBe('grocery and TEST');
expect(log.mock.calls[1][0]).toBe('apple and TEST');
expect(log.mock.calls[2][0]).toBe('banana and TEST');
});

Observations:

  • We created the integration test at the root of the project as it tests across the packages.
  • It is the calls to unmock that make this an integration test; the test uses the apple and banana code when loading grocery.

We can now run the tests with:

npm run test

note: Jest warns of duplicate mocks for sillyname when running the integration tests; because there are duplicate mocks. Given the unit tests need their own mocks, I do not see a way around this warning.

Next Steps

We wrap up our monorepo exploration in the next article, Monorepos By Example: Part 3.

--

--