How to Unit Test Express Controllers with Mocha and Chai
Most unit tests? Aren’t unit tests. They’re integration tests. Anytime you see someone spin up a mongodb memory server? They’re checking to make sure that their controllers integrate with MongoDB. This is great. But it’s not a unit test.
Unit tests utilize spies, stubs, and mocks to test ONLY a single unit of code.
There are lots of different ways to organize your MERN stack application. The way I’ll be using in this article distinguishes between routes and controllers. Routes only handle routing. i.e. this URI should call this function. That function? Is a controller.
Our restaurantController.js is setup as a module exporting multiple functions or methods. We’re going to start by testing the findById method.
Think through the logic.
findByID calls the Mongoose model method findByID. mongoose.Model.findById returns a promise. That’s right, it doesn’t actually return data, it returns a promise.
The promise can resolve or reject. If it is resolved, it should return data from the database. If it is rejected, it should return the error.
So what do we want to test? We want to test that if we call the module findById method, it calls the Mongoose method, and if successful, it should call res.json with the resolved value.
If we call the module findById method, it still calls the Mongoose method, but if an error occurs, it should call res.status with an error code, and it should still call res.json with the resolved value.
If you enjoyed this post or found it helpful, would you do me a favor and by follow me here, on Medium?
Test setup
Install mocha and chai as dev dependencies. While we’re at it, I’m going to also install sinon to help with our stubbing, and sinonAsChai.
Next, I’ll setup a test folder at the root of my server. From here, I want to include the same folder structure as my app for easier mapping purposes.
Then, I’ll setup a test file with all the necessary imports, and a describe block for the controller, nesting a describe block for the specific method on that controller. Then, I’ll add two it blocks for the two scenarios we discussed above.
Finally, I’m going to add a new script to my package.json to call mocha as the test runner and point it at the correct folder.
Writing the Tests
Especially when you’re first starting, I recommend the “Triple-A” method:
Arrange
Act
Assert
Will this leave you with some duplicated code? Yes. But you can always go back and refactor once you get things working.
Arrange the findById arguments, req and res. I know that findById takes in two parameters, req and res. Normally, these would be passed in by express. But I don’t want to test express (integration). I only want to test my specific functions. So I’m going to mock up objects that only have the properties I need.
Then, I’m going to act. I call the controller method and pass in my new req and res.
We will use Chai (an assertion library) to help us assert that res.json should be called.
Now, we go back to our arrangement and we stub out the Mongoose model method. Note: You may find easier ways to do this depending on how you write your db connections. Because I am pulling in the entire models directory here, I’ve found it easier to simply mock the Model constructor.
What is a sandbox? Sinon allows us to create stubs in a sandbox. It’s a self-contained area where our changes will not affect others.
Now, our code should look something like this.
Running the Tests
All that’s left to do is write a test script. I added
“test:mocha”: “mocha ‘test/**/*.spec.js’”
to my package.json.
Our tests pass, but how can we be sure they are right? Simple, we’ll delete some of the code to see if they fail.
In Test Driven Development, we would write these tests before any code was written, letting us start with failing tests. Here, we’ll have to reverse engineer the process.
By commenting out the .then promise block which contains res.json and running the test, we see that the test does indeed fail.
We do a similar thing with the catch block, and we have reasonable certainty that our controller method findById is covered.
Homework (Optional)
Your homework: see if you can write tests for the other controller methods.
Note: If you get stuck, you can find my completed controller tests here: https://github.com/jonathanjwatson/summer-cicd-mern/blob/main/test/controllers/restaurantController.spec.js