Jest + Chai and expect.assertions.

At Transparent Classroom, we use both Mocha/Chai to test our modules and Redux code and Jest to test our React components. Mocha/Chai and Jest are very similar in syntax, except for this annoying difference in that the matchers are different, e.g. to.eq(1) vs toEqual(1). We liked the natural assertion style of Chai, which is also similar to RSpec (which we use extensively for our Rails backend), so we wanted to use Chai-style assertions in our Jest tests. This post explains how to make that possible, still take advantage of some neat new features of Jest's assertion library, and learn something about Object.defineProperty.

Getting Started.

The setup was originally inspired by this post by Ruben Oostinga. You should read through that post for context. The key takeaway is to do two things:

1. Create a setup-test-framework-script.js.

Create setup-test-framework-script.js and place it in your root directory.

const chai = require('chai')

// Make sure chai and jasmine ".not" play nice together
const originalNot = Object.getOwnPropertyDescriptor(chai.Assertion.prototype, 'not').get  
Object.defineProperty(chai.Assertion.prototype, 'not', {  
  get() {
    Object.assign(this, this.assignedNot)
    return originalNot.apply(this)
  },
  set(newNot) {
    this.assignedNot = newNot
    return newNot
  },
})

// Combine both jest and chai matchers on expect
const originalExpect = global.expect

global.expect = (actual) => {  
  const originalMatchers = originalExpect(actual)
  const chaiMatchers = chai.expect(actual)
  const combinedMatchers = Object.assign(chaiMatchers, originalMatchers)
  return combinedMatchers
}

2. Add the script to your Jest configuration.

Inside package.json, under your "jest" configuration:

  "jest": {
    ...
    "setupTestFrameworkScriptFile": "<rootDir>/setup-test-framework-script.js",
  }

Huzzah, We're Done! Well, Not Quite...

This worked great, for a long while. We could happily write expect(1).to.eq(1) in our Jest tests and everyone was happy.

UNTIL.

I came across a neat feature of Jest, where you can assert the number of assertions that should be made. This is useful when you're testing async actions, and want to make sure your assertions are actually fired. Otherwise, if you have a test like this:

it('should work', () => {  
  return somethingAsyncAndReject().then(() => {
    expect(1).to.eq(2)
  })
})

and somethingAsync() rejects its promise, this test would silently pass because the .then block was never reached. Sad times. Jest fixes the issue by allowing you to specify that assertions should be made:

it('should work', () => {  
  expect.assertions(1)
  return somethingAsyncAndReject().then(() => {
    expect(1).to.eq(2)
  })
})

Then you can get output like this to the console:

expect.assertions(1)

Expected: one assertion  
Received: zero assertions  
Expected :1  
Actual   :0  

At least, that's how it works in theory. Using the super-charged, monkey-patched global.expect object in our setup, we get an error:

TypeError: expect.assertions is not a function  

This was because in our setup, we set global.expect to only be a function. Recall this snippet:

global.expect = (actual) => {  
  ...
}

There ain't no .assertions function on that! We've blindly overridden some useful functionality of expect that Jest provided us. Fortunately, you can give those methods back to the object:

global.expect = (actual) => {  
  ...
}
Object.keys(originalExpect).forEach(key => (global.expect[key] = originalExpect[key]))  

This allows us to set an expectation around the number of assertions that will be made, but my code was still mysteriously failing, even with this simple example:

it('should work', () => {  
  expect.assertions(1)
  expect(1).to.eq(1)
})

I was still receiving an error saying that 1 assertion was expected, but none were made.

Any guesses as to why?

I didn't have any either, and it took me the better part of an hour to figure out.

expect#assertions isn't actually magic, which should come as no surprise. The implementation of it is quite simple, it sets a state variable of the number of expected assertions. After some digging around, I figured out that the variable tracking the number of assertions made was also on that state, called assertionsMade. This variable would be incremented when assertions are made using Jest matchers, but obviously Chai would not do the same, hence the discrepancy. If I had written expect(1).toEqual(1) it would have worked fine, but then I miss out on my original goal of enjoying the natural syntax of Chai and not remembering yet another syntax.

I needed a way to tell the Chai matchers to also increment that value whenever I used it. I knew how to increment the value using this handy method:

originalExpect.setState({ assertionsMade: assertionsMade + 1 })  

Telling Chai to run this statement whenever its matchers were accessed was a trickier matter. In comes Object.defineProperty to the rescue! It allows us, among other things, to do something specific whenever that a property is accessed on an object. Essentially it works like this:

const myObj = {}  
Object.defineProperty(myObj, 'foo', {  
  get() {
    console.log('hello!')
    return 42
  }
})
myObj.foo // logs "hello!", returns 42  

This setup allows us to effectively create middleware on an object's properties, with potential use cases like logging the number of times it was accessed. While I don't recommend using it regularly in practice, since Object.defineProperty is clever, it does exactly what we need in this case where we're hacking some functionality together.

With Chai, we always end up using .to in our assertions, so it seemed like a reasonable place to stick our little "middleware." Here's the final code:

global.expect = (actual) => {  
  const originalMatchers = originalExpect(actual)
  const chaiMatchers = chai.expect(actual)

  // Add middleware to Chai matchers to increment Jest assertions made
  const { assertionsMade } = originalExpect.getState()
  Object.defineProperty(chaiMatchers, 'to', {
    get() {
      originalExpect.setState({ assertionsMade: assertionsMade + 1 })
      return chai.expect(actual)
    },
  })

  const combinedMatchers = Object.assign(chaiMatchers, originalMatchers)
  return combinedMatchers
}
Object.keys(originalExpect).forEach(key => (global.expect[key] = originalExpect[key]))  

Note that we return chai.expect(actual), i.e. the original intended value. We can't return chaiMatchers.to because that creates an infinite loop (whee!).

With this in your setup file, you have everything needed to make assertions about how many assertions should be made while still using the Chai syntax in your Jest tests.

I hope you enjoyed this article and learned something interesting in the process! Feedback is always welcome.