DEV Community

Zac Haluza
Zac Haluza

Posted on • Edited on

JavaScript Unit Testing 101: Mocha & Chai ☕️

Note: this entire lesson (with testable files!) is available as a GitHub repo!


This post is a simple walkthrough and reference for anyone interested in
learning the basics of creating unit tests for JavaScript using Mocha and Chai.

Why Mocha & Chai?

In terms of technology, successful JS testing requires three things:

  • Testing environment/test runner
  • Testing framework
  • Assertion library Different frameworks assume different combinations of these roles. Possible combinations include…
  • Mocha JS (testing environment & framework) + Chai (assertion library)
  • Jest (all-in-one)
  • Jasmine (all-in-one)

This guide will use a combination of Mocha and Chai. However, these kinds of tests can also be run with other testing solutions like Jest. The approach is identical.

What's Test-Driven Development?

Test-driven development (or TDD) is a form of development in which developers write tests for their code before they actually write the code.

TDD utilizes "red-green testing": you initially write a test that will fail (red), then write and adjust your code so that the test passes (green).

Here are some of the benefits of TDD:

  • It’s more efficient
    • You don’t have to write new tests or rewrite tests after making changes — because you’ve already written tests to anticipate the nature of your code
  • You write better code
    • Since you plan everything out beforehand, your code is better organized
    • It’s more testable — you don’t have to rewrite or refactor anything for tests
    • Fewer bugs — you’ll catch them sooner, and you’ll also have regression testing built in (all the tests you’ve ever run will get re-run when you make a change)
  • You’ll have great code coverage
    • Your tests are written to cover the general breadth of your code

Unit Testing Walkthrough

Intro

In the guide below, you'll conduct some basic unit tests. Unit tests focus on
individual parts of the code, and they're likely the most common types of tests you'll perform as a developer.

They're different from integration tests, which test whether a program or app functions properly as a whole.

Let's Get Started!

Setting up Mocha & Chai

First, set up a package.json file and import all needed dependencies.

npm init -y
npm i mocha chai --save-dev
Enter fullscreen mode Exit fullscreen mode

Next, install Babel to make sure all JS is backwards compatible.

npm i @babel/core @babel/preset-env @babel/register --save-dev
Enter fullscreen mode Exit fullscreen mode

Create a Babel file: .babelrc
This will make sure that Babel functions correctly.

{
    "presets": ["@babel/preset-env"]
}
Enter fullscreen mode Exit fullscreen mode

Setting Up Our Tests

Create a src folder. Inside it, create two files:

  • .js file: This will contain the JavaScript we want to test
  • test.js file: This will contain our tests

Following TDD principles, the .js folder will be blank. Create a basic test
inside the test.js folder:

import { expect } from 'chai';
import { getLetterCount } from './letter-count';

describe('getLetterCount - basic functionality', () => {
  it('returns an empty object when passed an empty string', () => {
    const expected = {};
    const actual = getLetterCount('');
    expect(actual).to.deep.equal(expected);
  });
});
Enter fullscreen mode Exit fullscreen mode

Running Our Tests

Now run the test. First set up the testing script in the package.json file.

...
"scripts": {
"test": "npx mocha \"src/\*_/_.test.js\" --recursive --require @babel/register"
},
...
Enter fullscreen mode Exit fullscreen mode

Next, run the test.

npm test

// or "npm run test"
Enter fullscreen mode Exit fullscreen mode

Now, set up a function in the .js file that will make the test pass.

export const getLetterCount = string => ({});
Enter fullscreen mode Exit fullscreen mode

Refining Our Tests

Now we need to add tests until we see failure again.

import { expect } from 'chai';
import { getLetterCount } from './letter-count';

describe('getLetterCount - basic functionality', () => {
  it('returns an empty object when passed an empty string', () => {
    const expected = {};
    const actual = getLetterCount('');
    expect(actual).to.deep.equal(expected);
  });
  it('return correct letter count for a word with only one of each letter', () => {
    const expected = { c: 1, a: 1, t: 1 };
    const actual = getLetterCount('cat');
    expect(actual).to.deep.equal(expected);
  });
});
Enter fullscreen mode Exit fullscreen mode

Now let’s build out our function so it passes:

export const getLetterCount = string => {
  const letters = string.split('');
  let letterCount = {};
  letters.forEach(letter => {
    letterCount[letter]
      ? (letterCount[letter] += 1)
      : (letterCount[letter] = 1);
  });
  return letterCount;
};
Enter fullscreen mode Exit fullscreen mode

This is good, but let’s make sure our test passes when we use more complex words, i.e. words with more than one of a certain letter.

import { expect } from 'chai';
import { getLetterCount } from './letter-count';

describe('getLetterCount - basic functionality', () => {
  it('returns an empty object when passed an empty string', () => {
    const expected = {};
    const actual = getLetterCount('');
    expect(actual).to.deep.equal(expected);
  });
  it('return correct letter count for a word with only one of each letter', () => {
    const expected = { c: 1, a: 1, t: 1 };
    const actual = getLetterCount('cat');
    expect(actual).to.deep.equal(expected);
  });

  it('return correct letter count for words with more than one of a certain letter', () => {
    const expected = { m: 1, i: 4, s: 4, p: 2 };
    const actual = getLetterCount('mississippi');
    expect(actual).to.deep.equal(expected);
  });
});
Enter fullscreen mode Exit fullscreen mode

More Practice: isPalindrome

Now test and create a function called isPalindrome. It should take two arguments and return “true” if the arguments are palindromes of one another. As a bonus, have the function return “true” even if…

  • the arguments contain spaces
  • the arguments utilize different types of capitalization (e.g. "listen" & "SILENT")

Practice utilizing the principles described above to build out tests that cover any and all relevant use cases, including edge cases. Reference the repo files if you need a hint — note that your solution and test cases may differ depending on your approach (e.g. a much simpler solution would be to utilize Lodash).

What are your thoughts on unit testing and test-driven development? Do you test your code? Share your thoughts below!

(Note: Much of the code referenced here has been adapted from the Unit Testing section of Shaun Wassel's JavaScript: Test-Driven Development (ES6) course on Lynda.)

Top comments (0)