Better test structuring using Mocha's context()

Corey Cleary - Nov 4 '22 - - Dev Community

Originally published at coreycleary.me. This is a cross-post from my content blog. I publish new content every week or two, and you can sign up to my newsletter if you'd like to receive my articles directly to your inbox! I also regularly send cheatsheets and other freebies.

Sometimes when working on adding or modifying unit tests within a test suite, you may not be sure what the best way to structure the tests within that suite is.

The tests may be getting cluttered, out of control, and you may have repetition.

But better organization will lead to making writing tests less painful (and even enjoyable) because they're easier to maintain.
And since you're usually working on code within a team, it will make it easier for everyone on that team to maintain, understand what the tests are testing, and to add new tests as well.

If you're using for your tests, one way to do this is by using the context() function. You're almost definitely already using describe() to group your test cases, but context() will provide for more explicit grouping of tests within your suite.

Example

Take a look at the example below, that is only using describe() for grouping tests:

describe('Array', () => {
  describe('indexOf()', () => {
    it('should not throw an error when the array item is not present', () => {
      (() => {
        [1, 2, 3].indexOf(4)
      }.should.not.throw())
    })

    it('should return -1 when the array item is not present', () => {
      [1, 2, 3].indexOf(4).should.equal(-1)
    })

    it('should return the index where the element first appears in the array', () => {
      [1, 2, 3].indexOf(3).should.equal(2)
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

You'll notice that the phrase "when the array item is not present" is repeated twice, in the 1st and 2nd tests.

It was kind of annoying to type that twice, and from reading these tests you don't immediately pick up on the fact that those tests are related. If you have business requirements for a feature you're building, you might even have requirements like "when X is not present, do Y" and "when X is not present and A, do B".

Instead it would be great if we could group these together. Now, we could use another describe() to do this, like so:

  describe('indexOf()', () => {
    describe('when not present', () => {
      it('should not throw an error when the array item is not present', () => {
        (() => {
          [1, 2, 3].indexOf(4)
        }.should.not.throw())
      })

      it('should return -1 when the array item is not present', () => {
        [1, 2, 3].indexOf(4).should.equal(-1)
      })
    })

    it('should return the index where the element first appears in the array', () => {
      [1, 2, 3].indexOf(3).should.equal(2)
    })
  })
Enter fullscreen mode Exit fullscreen mode

But then it sort of begs the question... what is the difference between the nested describe() block and the other individual it() blocks within the same "parent" describe('indexOf()', () => {})?

This is where the context() function comes in. We can instead use it to group the contextually similar test cases, and have them still be within our "parent" unit test describe() block.

Take a look at the example above, rewritten below using it:

describe('Array', () => {
  describe('indexOf()', () => {
    context('when not present', () => {
      it('should not throw an error', () => {
        (() => {
          [1, 2, 3].indexOf(4)
        }.should.not.throw())
      })

      it('should return -1', () => {
        [1, 2, 3].indexOf(4).should.equal(-1)
      })
    })

    context('when present', () => {
      it('should return the index where the element first appears in the array', () => {
        [1, 2, 3].indexOf(3).should.equal(2)
      })
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

context() is just an alias for describe(), so it works exactly the same and is the same function.
From a test runner perspective this doesn't do anything - it doesn't change the way the tests are run, the order they're run in, etc.

But BDD-style tests are designed to be readable to even non-developers (like QA and business analysts/product managers).
So having an "extra" function we can use to make it clearer how the tests are grouped and what a given "test block" is testing is a huge boost for readability.

When to use context?

A good heuristic for when to use context() over describe() is when you repeat yourself across tests.
That's a key indicator that what you're dealing with is, in fact, a contextual set of tests, and that they can likely be grouped together.

Not everything needs to be grouped using it, I certainly don't do that. But when you have the following:

  • you're repeating the same language in your test cases
  • you're starting to have many test cases within a single "parent" unit, and that starts getting out of control, and can group some into contextually similar sub-groups

...context() can come in very handy.

It will make your test suites easier to read, your teammates will understand what the intent of the tests are, and it can help the tests better serve as documentation of requirements/functionality.

Love JavaScript but still getting tripped up by unit testing, architecture, etc? I publish articles on JavaScript and Node every 1-2 weeks, so if you want to receive all new articles directly to your inbox, here's that link again to subscribe to my newsletter!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .