JS monorepos in prod 4: unit testing with Mocha and Should.js
By David WORMS
Feb 25, 2021
Never miss our publications about Open Source, big data and distributed systems, low frequency of one email every two months.
Unit testing is essential for every long-term project and allows you to pull down functionalities of your code into isolated testable units. Indeed the main goal of a unit test is to verify if an independent piece of code compiles to the expected behavior. As such unit tests should have a narrow scope and cover all possible cases.
Herein we will cover and compare unit testing in both JavaScript and CoffeeScript using the popular Mocha testing framework in combination with the assertion library Should.js . In the next article of our series we will be discussing CI/CD integration in the context of monorepos:
- Part 1: project initialization
- Part 2: versioning and publishing strategies
- Part 3: commit enforcement and changelog generation
- Part 4: unit testing with Mocha and Should.js
- Part 5: merging Git repositories and preserve commit history
- Part 6: CI/CD, continuous integration and deployment with Travis CI
- Part 7: CI/CD, continuous integration and deployment with GitHub Actions
Unit testing with Mocha and Should.js
While the gatsby-remark-title-to-frontmatter
has been used for some time across several of our websites and can be considered stable, it seems awkward to release a stable version 1.0.0 without shipping any unit tests.
I usually start by writing unit tests before starting any development. What a shame isn’t it? Not really. This practice brings some advantages in the long term. Indeed covering your codebase with a comprehensive set of unit tests will definitively help to keep your code clean and functional and will smoothen any refactoring process.
As stated above we are going to use Mocha for unit testing and Should.js for the assertions. But feel free to use your favorite tools. Indeed there are several alternative libraries and tools including Jest and Chai. Before starting with our example let’s first move inside the gatsby-remark/title-to-frontmatter
directory:
cd gatsby-remark/title-to-frontmatter
Next we need to add Mocha and Should.js to our package dependencies:
yarn add -D mocha should
As you can see the yarn
command was executed with a few arguments. The -D
or --dev
argument permits the installation of one or more packages in the devDependencies
of the packages.json
found in our gatsby-remark/title-to-frontmatter
package.
Additionally, it is convenient to modify Mocha configuration in such a way that each time you import it will automatically load the assertion library Should.js. There are multiple places where you can do that but I alter the package.json
file as following:
{
...
"mocha": {
"throw-deprecation": true,
"require": [
"should"
],
"inline-diffs": true,
"timeout": 40000,
"reporter": "spec",
"recursive": true
}
}
Now Should.js will be automatically loaded with mocha
thanks to the require
property. Consequently we won’t need to require it inside our test modules.
Unit tests are single functions. In Mocha, it is the it
function. Test functions are grouped with the describe function. A test should cover one feature in the most readable and precise manner. When possible, they should be autonomous without requiring external cues but instead, they should recreate the conditions of their success. I prefer to have everything in my test functions and avoid fixtures stored in a different location. However, every project has its specificities and it is not always applicable. But, when possible, I find it extremely convenient to have a unified view of the input conditions, the subject being tested, and the output assertion. All in one place without having to open another file.
To illustrate this, we use the title-to-frontmatter
package created in the first article of this serie. Briefly title-to-frontmatter
parses a markdown document, removes the title from it, and places it inside the frontmatter
object. The code is located in the ./packages/title-to-frontmatter
directory.
To test the “./lib/index.js” module, we write a test in test/index.js
file using both Mocha and Should.js. The test looks like this:
const Remark = require('remark')
const toHtml = require('hast-util-to-html')
const toHast = require('mdast-util-to-hast')
const extractTitle = require('..')
describe( 'Extract title', () => {
it( 'Move the title to frontmatter', () => {
// Initialize
const mast = (new Remark()).parse(
[
'# this is the title',
'and some text'
].join('\n')
)
const frontmatter = {}
// Run
extractTitle({
markdownNode: {
frontmatter: frontmatter
},
markdownAST: mast
}, {})
// Convert
const hast = toHast(mast)
const html = toHtml(hast)
// Assert
html.should.eql('<p>and some text</p>')
frontmatter.should.eql({
title: 'this is the title'
})
})
})
Before executing the test, we first need to add the necessary dependencies:
yarn add -D \
remark \
hast-util-to-html \
mdast-util-to-hast
Once done we can run our test:
yarn mocha test/index.js
yarn run v1.22.5
$ /Users/david/projects/github/remark-gatsby-plugins/node_modules/.bin/mocha test/index.js
Extract title
✓ Move the title to frontmatter
1 passing (13ms)
✨ Done in 0.56s.
The test passed. We are good and ready to commit:
git add package.json test/index.js
git commit -m "test(gatsby-remark-title-to-frontmatter): move the title to frontmatter"
Project structure with tests
Usually, it is good practice to align the structure of your test folder with the structure of your source code. There are mostly three strategies for placing your tests.
Some developers include the tests directly next to the source code. The tests can then be executed under certain conditions like with the presence of a certain environment variable or when the module is exected directly instead of being required.
It is also possible to place the tests next to tested module. Using a naming convention like ./lib/my_module.test.js
, a globing expression similar to node lib/**/*.test.*
only execute the unit tests.
Finally, my prefered approach, that I used above, is to place the tests inside a dedicated folder like ./test
. The test ./test/my_module.js
will test the ./lib/my_module.js
module. If multiple aspects of the module were tested, you can also place multiple tests inside it folder named after your module, for example inside ./test/my_module
.
Unit testing with CoffeeScript
I prefer to use CoffeeScript when writing my tests. This is the approach I took for the CSV parser package. The source code is written in JavaScript while the tests are in CoffeeScript. It makes the code for tests shorter and much more expressive. Let’s integrate CoffeeScript and convert our test:
yarn add -D coffeescript
Inside the Mocha configuration present in the package.json
file, add coffeescript/register
:
{
"mocha": {
"throw-deprecation": true,
"require": [
"should",
"coffeescript/register" ],
"inline-diffs": true,
"timeout": 40000,
"reporter": "spec",
"recursive": true
}
}
Like with Should.js, CoffeeScript is registered in Mocha. This way, the code is automatically transpiled from CoffeeScript to JavaScript before being executed by the Node.js engine. Now, we can convert our JavaScript code to CoffeeScript:
Remark = require 'remark'
toHtml = require 'hast-util-to-html'
toHast = require 'mdast-util-to-hast'
extractTitle = require '..'
describe 'Extract title', ->
it 'Move the title to frontmatter', ->
# Initialize
mast = (new Remark()).parse """
# this is the title
and some text
"""
frontmatter = {}
# Run
extractTitle
markdownNode:
frontmatter: frontmatter
markdownAST: mast
, {}
# Convert
hast = toHast mast
html = toHtml hast
# Assert
html.should.eql '<p>and some text</p>'
frontmatter.should.eql
title: 'this is the title'
I will leave the comparison with the original test/index.js
file to your judgment, we all have different tastes. To execute all the tests, we can now register the yarn test
command inside the package.json
file of our title-to-frontmatter
package. Assuming the tests are written in CoffeeScript (otherwise, change the .coffee
extension to .js
in the globing expression) the command should look like the following:
{
"scripts": {
"test": "mocha 'test/**/*.coffee'"
}
}
Running the yarn test
(or npm test
) command produces the same output as with the mocha
command:
yarn test
yarn run v1.22.5
$ mocha 'test/**/*.coffee'
Extract title
✓ Move the title to frontmatter
1 passing (11ms)
✨ Done in 0.83s.
All good, we can now commit our changes:
git rm test/index.js
git add package.json test/index.coffee
git commit -m "test(gatsby-remark-title-to-frontmatter): convert test to coffee"
Leveraging lerna
for unit testing
Until now we have just demonstrated how to run the tests in one package. Navigating through each package to run each test is time consuming and error prone, especially if your monorepos is composed of a lot of packages. Leveraging Lerna, a single command is used to run all the tests with help of the lerna run
command:
lerna run <script> -- [...args]
This command runs a NPM script in each package that contains that script. As shown previously, our package contains an aliased NPM script for test in its package.json
file:
{
...
"scripts": {
"test": "mocha 'test/**/*.coffee'"
}
}
Consequently running the following command should run all the tests:
lerna run test
info cli using local version of lerna
lerna notice cli v3.22.1
lerna info versioning independent
lerna info Executing command in 1 package: "yarn run test"
lerna info run Ran npm script 'test' in 'gatsby-remark-title-to-frontmatte
r' in 0.5s:
yarn run v1.22.10
$ mocha 'test/**/*.coffee'
Extract title
✓ Move the title to frontmatter
1 passing (8ms)
Done in 0.41s.
lerna success run Ran npm script 'test' in 1 package in 0.5s:
lerna success - gatsby-remark-title-to-frontmatter
As you can see in the command output the mocha 'test/**/*.coffee'
has been run in our tested package. Fittingly the more packages and tests you will have and the more scripts lerna
will execute.
We can do now one more thing to polish our approach. To avoid entering the lerna
command each time, we can modify the package.json
found in the root of our monorepos as following:
{
"scripts": {
"postinstall": "husky install",
"publish": "lerna publish from-git --yes",
"test": "lerna run test" }
}
After aliasing our "test"
script with "lerna run test"
, we use the yarn test
command to run our tests.
Cheatsheet
- Install Mocha and Should.js:
yarn add -D mocha should # If using CoffeeScript yarn add -D coffeescript
- Mocha configuration inside
package.json
:{ "mocha": { "throw-deprecation": true, "require": [ "should", "coffeescript/register" ], "inline-diffs": true, "timeout": 40000, "reporter": "spec", "recursive": true } }
- Mocha template in JavaScript:
describe( 'Group description', () => { it( 'Test description', () => { // Write your test }) })
- Mocha template in Coffee:
describe 'Group description', -> it 'Test description', -> # Write your test
- Register a
test
script inpackage.json
:{ "scripts": { "test": "mocha 'test/**/*.coffee'" } }
- Register a
test
script to run tests from all packages with Lerna:{ "scripts": { "test": "lerna run test" } }
Conclusion
We have briefly talked about some good practices when using unit testing with Mocha framework and Should.js. We showed how we could write tests in both JavaScript and CoffeeScript. The latter only required to add the appropriate package and configure Mocha to use it in the package.json
file. Apart from some aesthetic and expressiveness differences between JavaScript and CoffeeScript writing tests remain similar in both languages. However, I value the expressiveness and readability of CoffeeScript and as such, it remains my go-to language for writing tests. Finally, keep in mind that writing and configuring unit tests in monorepos is not different from regular repositories. Just ensure that each test folders remain in the correct package and harness the lerna run
command to run tests present in all of your packages.
In our next article, we will see that whereas implementing tests in monorepos is pretty easy the integration of CI/CD pipeline to automate the publication of packages requires a few more settings, that will be covered extensively.