Testing Your Style With ESLint and Mocha

Blake Williams

I’ve been working on a front-end application that uses ESLint for linting and Mocha as our testing framework. We’ve tried a few solutions to integrate linting into our workflow but it can easily be ignored. To make sure linting errors were given the attention they deserved we decided to turn linting errors into test failures.

Generating tests

ESLint provides all the tools we need to lint our files and get the results back directly in JavaScript.

To get started, create a file called called eslint-test.js in your test directory.

The first thing we need to do is collect an array of file paths we want to lint and get the results from ESLint after telling it our environment information and to use our .eslintrc.

// eslint-test.js
import glob from 'glob';
import { CLIEngine } from 'eslint';
import { assert } from 'chai';

const paths = glob.sync('./+(app|test)/**/*.js');
const engine = new CLIEngine({
  envs: ['node', 'mocha'],
  useEslintrc: true,
});

const results = engine.executeOnFiles(paths).results;

Now that we have the results we can define our describe block and generate tests in that block. Let’s add that to the end of our eslint-test.js file.

describe('ESLint', function() {
  results.forEach((result) => generateTest(result));
});

For each result we want to extract the filePath and messages. filePath will be used to generate a good test name. We’ll use messages to check if we have any messages and if we do, we fail the test with a well formatted reason built from messages.

function generateTest(result) {
  const { filePath, messages } = result;

  it(`validates ${filePath}`, function() {
    if (messages.length > 0) {
      assert.fail(false, true, formatMessages(messages));
    }
  });
}

Finally we can loop over each of the messages and generate a failure message telling us the line number, column number, failure message, and the ESLint rule name.

function formatMessages(messages) {
  const errors = messages.map((message) => {
    return `${message.line}:${message.column} ${message.message.slice(0, -1)} - ${message.ruleId}\n`;
  });

  return `\n${errors.join('')}`;
}

Here’s the test generator code all together:

import glob from 'glob';
import { CLIEngine } from 'eslint';
import { assert } from 'chai';

const paths = glob.sync('./+(app|test)/**/*.js');
const engine = new CLIEngine({
  envs: ['node', 'mocha'],
  useEslintrc: true,
});

const results = engine.executeOnFiles(paths).results;

describe('ESLint', function() {
  results.forEach((result) => generateTest(result));
});

function generateTest(result) {
  const { filePath, messages } = result;

  it(`validates ${filePath}`, function() {
    if (messages.length > 0) {
      assert.fail(false, true, formatMessages(messages));
    }
  });
}

function formatMessages(messages) {
  const errors = messages.map((message) => {
    return `${message.line}:${message.column} ${message.message.slice(0, -1)} - ${message.ruleId}\n`;
  });

  return `\n${errors.join('')}`;
}

The results

Now that we have our ESLint test generator we can run our test suite and see it in action.

$ mocha
  1) ESLint validates ./app/index.js
     AssertionError:
44:35 Missing semicolon - semi

It works! Our failure tells us the failure’s file, line number, column number, and linting error!

Conclusion

This combined with GitHub’s required checks can make it much harder to merge code with poor style in your application. You can also take this same idea and apply it to other tools or linters like Hound.