Quality JavaScript projects: testing and linting

Photo by Volkan Olmez on Unsplash

Thanks to our friend the second law of thermodynamics, every aspect of our lives tends to turn into chaos if not subject to proper maintenance and dedication. Software projects are not an exception and I do not need to tell you how easy it is for a software product to evolve into an awful buggy mess. Fortunately, nowadays we have a set of validated practices that help and tell us how to create, design and maintain quality software.

Applying these techniques to our JavaScript projects is not harder than in any other language. I would say it’s even easier, thanks to the vast amount of libraries and tools out there and the active community.

Here at Bluekiri we are in the process of improving quality in our JavaScript projects using best practices. Let me show what we are aiming for…

Testing

As developers, the most common scenario for us is writing unit tests or integration tests. Ocassionally, you might find yourself writing other types of tests, such as functional or E2E tests. I will mainly focus on unit testing here.

Mocha or Jest?

Among JavasScript testing frameworks, I’ve happily used Mocha for years. It is simple, fast, stable and widely adopted. Mocha allows us to easily create test suites (not necessarily unit tests, but also integration tests, and even functional tests). This simplicity comes with a small disadvantage: if your need other testing features, you’ll have to spend more time installing and configuring additional tools.

I recently gave Jest a try. It feels a bit slower than Mocha, but I like the fact that many features are already integrated and the setup is a piece of cake. For example, I am a big fan of desktop notifications while developing. Sometimes the terminal where you run tests is minified, or not visible in your desktop at the exact moment you make a change in your code. With desktop notifications, you’ll always have instant visual feedback if you break something, which is specially helpful for TDD and its red-green-refactor cycle. However, if you want desktop notifications with Mocha, you’ll need to install additional libraries, which differ across plaforms (Linux, Windows and Mac).

Jest also includes built-in code coverage reports, so, for example, you can control whether or not your CI builds pass based on these reports. When working with Mocha, you would need to install coverage tools, such as Istanbul. That being said, Jest’s built-in code coverage is based on Istanbul underneath, so the only difference is that Jest installs it for you and saves you the hassle of writing some scripts for code-coverage checking.

In addition to using Mocha or Jest, I also use Chai as a way to write more readable assertions. The more readable your tests are, the better. Why? Keeping your tests readable makes them easier to maintain. If your tests are easy to maintain, they are less likely to be abandoned.

Code linting

Code linting ensures that our team’s coding standards are met. Keeping a stable coding standard gives us many benefits such as code quality, readiblity or consistency. It makes it easier for developers to share knowledge, grab code from other colleagues, or in the case of interpreted languages, like JavaScript, protects us against syntax errors.

I have been using ESLint for a while. ESLint allows you to create your own coding standard definitions and also extend existing ones. For example, I sometimes extend Airbnb’s JavaScript Style Guide and adapt it to my needs. Here is an example of a .eslintrc file to define our own standard:

Additionaly, we can combine ESLint with plugins, such as Prettier, to autofix our linting errors.

NPM

So, now that we have our testing and linting tools installed and configured, we need to have a set of task or script definitions that integrates these tools with our development and build cycles. Basically this means creating a set of scripts necessary for all your development and build tasks. While Gulp or Grunt are certainly an option, I also think they add unnecessary abstractions and introduce more dependencies in your project. My preference here is to use NPM scripts to end up with something like this in our package.json:

"scripts": {
    "start": "node src",
    "start:watch": "nodemon src",
    "test": "jest test --collectCoverage --ci",
    "test:watch": "jest test --watchAll --notify",
    "lint": "eslint .",
    "lint:fix": "eslint --fix ."
}

What did we achieve here? Well, now, we have all these commands at our disposal, so for example, when I start making changes to my code, I will probably run npm run start:watch to launch my app in development mode, and, in another terminal, npm run test:watch to run my continuous testing script so that tests are run every time I make a change to my source code.

Our CI environment should also benefit from these scripts and base build definitions on them. A hypothetic testing stage would execute something like this:

npm install
npm run lint
npm test

Which lead us to my last point for today…

Continuous integration

Be sure to run your tests and linter in your CI builds. This will ensure that your tests and your coding standards are always in use.

If you do not have an automated continous integration system, your tests and coding standards are doomed. My experience is that developers won’t run tests every time they make a change(for hundreds of reasons: unawareness, lazyness, bad memory, lack of time…), your tests and coding standards will be eventually abandoned, and all your previous efforts will have been a waste of time.

create-js-package

I’ve created a simple tool that bootstraps new JavaScript projects including all these tools installed and ready to use. The tool generates new projects with the following features included:

  • Unit testing with Jest and Chai.
  • Test watcher for continous testing.
  • Test coverage checking.
  • ES6 linting based on ESlint and Prettier.

You can go and take a look at the code or also install the tool via NPM to generate your own projects.

In the next post, I’ll talk more about CI and release management.