JS monorepos in prod 4: unit testing with Mocha and Should.js

JS monorepos in prod 4: unit testing with Mocha and Should.js

David WORMS

By David WORMS

Feb 25, 2021

Categories: DevOps & SRE, Front End | Tags: Monorepo, Node.js

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:

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 in package.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.

Canada - Morocco - France

International locations

10 rue de la Kasbah
2393 Rabbat
Canada

We are a team of Open Source enthusiasts doing consulting in Big Data, Cloud, DevOps, Data Engineering, Data Science…

We provide our customers with accurate insights on how to leverage technologies to convert their use cases to projects in production, how to reduce their costs and increase the time to market.

If you enjoy reading our publications and have an interest in what we do, contact us and we will be thrilled to cooperate with you.