Unit testing code with utility functions & AJAX calls

Maggie Love
Refinery29 Product & Engineering
8 min readMar 5, 2017

One of the most important skills I’ve learned as a software engineer at Refinery29 has been unit testing. In some ways, writing tests can be harder than writing the code that’s being tested — especially when the code you’re testing uses utility functions, asynchronous functions, or a combination. I wanted to share a few tips I picked up while testing AJAX requests, in particular: when to use stubs, what to do when the test finishes before checking that your conditions passed, and how to best structure a test file. I’ll also go through my earlier (wrong) attempts at these tests so you can see the thought process that led to the final version. Shout-out to R29ers Carlo, Johnny, Gonzo and Patty for helping me work through these tricky concepts!

The test subject

These unit tests were written for a module that appears on all Refinery29 stories in the shopping category. It pops up from the bottom of the screen with an invitation to the user to sign-up for our shopping newsletter. I focused on the email validation functionality, which gives the user a visual warning when she tries to submit a blank or invalid email:

The way this works is that when the sign-up button is clicked, a function checks to see if the value of the email address field is blank or invalid. Here's thehandleSubmit function in which this takes place (coded in React) :

handleSubmit() {    . . .    registerEmailSignUp({
email: this.state.value
})
.then(this.handleSuccess)
.catch(this.handleFailure);
}

registerEmailSignUp is a utility we’re importing. (This is a simplified version of the actual code; there are email sign-up modules for other types of stories on the site, and we can add key-value pairs when we call registerEmailSignUp to specify details for the different newsletters.) It checks for the email address’s validity to the best of our ability using a regular expression, and either signs the user up for the shopping newsletter, at which point handleSuccess is called, or returns a reason it was not possible to add the email to the list, at which point handleFailure is called. Since we’re testing what the module does with blank or invalid emails, we care about the handleFailure part.

In handleFailure, I use React’s setState function to change this.state.invalid from false to true. When the state is invalid, the input field and sign-up button have the invalid class added to them, which gives them a red outline and changes the text of the button to “TRY AGAIN” and changes the text of the input field to the reason sign-up was unsuccessful.

So there are two main aspects of the email validation UI to cover in tests: that the user is notified when the email is blank, and that the user is notified that the email is invalid. When I started writing tests, I knew I needed to check that the invalid class was added to the input field and sign-up button. I started with the easier of the two cases, which is not entering any email at all and then clicking the sign-up button.

Calling the util directly, a.k.a. The Wrong Way

In the repo where this code lives, we’re using Mocha as our testing framework, Chai for assertions, and Airbnb’s Enzyme testing utility for React, but hopefully the code’s pretty readable even if you haven’t worked with these specific tools.

describe('when the Sign Up button is hit and the input field is invalid', function() {
let shoppingModule;
let signupButton;
let input;
let data = {};

it('adds the "invalid" class to the button and the input when the email is blank', function() {
shoppingModule = componentInDom();
signupButton = shoppingModule.find('.shopping-email-submit');
input = shoppingModule.find('.shopping-email-input');
signupButton.simulate('click');
return registerEmailSignUp(data).catch(() => {
expect(signupButton.hasClass('invalid')).to.equal(true);
expect(input.hasClass('invalid')).to.equal(true);
});
});

Higher up in the file, I’ve imported the component I’m testing and defined componentInDom() as the rendered shopping sign-up module. I isolate the parts I want to test, the input and the submit button. Since I’m testing what happens when a user doesn’t enter an email, I can just simulate a click on the mock component. Below that — remember that this is wrong version — I thought I was forcing a failure with the registerEmailSignUp util. I then checked that once the failure happened, both the input and the submit button had the invalid class added. This test passes, but there are a few issues with it.

First of all, I’m actually calling registerEmailSignUp, which is a problem for a couple reasons. I want the function to fail in this case, but what if I wanted to test what happens when registerEmailSignUp is a success? That function hits an API. If for whatever reason the API was down, my test would fail for reasons unrelated to the functionality I wrote around what happens after the response is returned. While we do want to test registerEmailSignUp as well as the component, it’s important for tests to be specific so that when the fail, it’s easier to isolate the issue. (Which is why registerEmailSignUp has its own separate unit tests.)

It also doesn’t really make sense for me to simulate a click of the submit button —with the purpose of calling the registerEmailSignUp function— and then immediately call registerEmailSignUp function in the next line. Here the test passes because in the shopping sign-up module I’ve rendered, when I simulate a click of the submit button, the real registerEmailSignUp utility fails. Of course, there’s a better way to do this.

Using stubs, a.k.a. The Right Way

Thanks to a suggestion that came out of code review, I stubbed registerEmailSignUp using Sinon as a spying library and made it reject as it would for a failed sign-up:

describe('when the Sign Up button is hit and the input field is invalid', function() {
it('adds the "invalid" class to the button and input when the email is blank', function() {
const shoppingModule = componentInDom();
registerEmailStub.returns(Promise.reject({ message: 'No Email Address' }));
expect(shoppingModule.find('.shopping-email-submit').hasClass('invalid')).to.equal(false);
expect(shoppingModule.find('.shopping-email-input').hasClass('invalid')).to.equal(false);
shoppingModule.find('.shopping-email-submit').simulate('click');
expect(shoppingModule.find('.shopping-email-submit').hasClass('invalid')).to.equal(true);
expect(shoppingModule.find('.shopping-email-input').hasClass('invalid')).to.equal(true);
});
. . .

There are then two sets of statements testing the invalid class on the shoppingModule’s button and input field, one set before the click and one set after the click. This way, we can be sure that the invalid class is not added until after the click.

But..

This test still doesn’t work as expected. 💩 Asynchronicity strikes again! This test fails because it checks for the invalid class before the function passed into catch is executed. Enter Lodash’s defer utility function:

it('displays the invalid email input form view', function(done){
const shoppingModule = componentInDom();
registerEmailStub.returns(Promise.reject({ message: 'No Email Address' }));
expect(shoppingModule.find('.shopping-email-submit').hasClass('invalid')).to.equal(false);
expect(shoppingModule.find('.shopping-email-input').hasClass('invalid')).to.equal(false);
shoppingModule.find('.shopping-email-submit').simulate('click');
defer(() => {
expect(shoppingModule.find('.shopping-email-submit').hasClass('invalid')).to.equal(true);
expect(shoppingModule.find('.shopping-email-input').hasClass('invalid')).to.equal(true);
done();
});
});

The test above is finally working! 🎉 But why? defer is the equivalent of window.setTimeout(fn, 0) — it makes the test wait for the current call stack to be executed before checking for the invalid class. But defer alone isn’t enough to make the test pass, because the test is done running before it actually checks for the invalid class. When we add the done callback, it forces Mocha to evaluate the second check for the invalid class before moving on to the next test in the suite.

The test for an invalid email address is the same, but with a different rejection message.

Part Deux: The Double Click

There’s another important part of this user interaction that needs to be tested. As you can see in the screen recording of the interaction, when you click the sign-up button with no email or an invalid email in the input, the text of the button changes to “TRY AGAIN,” and when you click it a second time, the field clears (showing only the placeholder text) and the button text changes back to “SIGN UP.” In this case, clicking the button removes the invalid class:

handleSubmit() {
if (this.state.invalid) {
return this.setState({
value: '',
invalid: false,
buttonText: BUTTON_DEFAULT
});
}
registerEmailSignUp({
. . .

Sticking with the blank input field, I rewrote the test to include a second click:

it('adds the "invalid" class to the button and input when the email is blank; it then removes the class if called again', function(done) {
const shoppingModule = componentInDom();
registerEmailStub.returns(Promise.reject({ message: 'No Email Address' }));
expect(shoppingModule.find('.shopping-email-submit').hasClass('invalid')).to.equal(false);
expect(shoppingModule.find('.shopping-email-input').hasClass('invalid')).to.equal(false);
shoppingModule.find('.shopping-email-submit').simulate('click');
defer(() => {
expect(shoppingModule.find('.shopping-email-submit').hasClass('invalid')).to.equal(true);
expect(shoppingModule.find('.shopping-email-input').hasClass('invalid')).to.equal(true);
shoppingModule.find('.shopping-email-submit').simulate('click');
expect(shoppingModule.find('.shopping-email-submit').hasClass('invalid')).to.equal(false);
expect(shoppingModule.find('.shopping-email-input').hasClass('invalid')).to.equal(false);
done();
});
});

As you can see, here I’m simulating the second click inside the defer block, then testing for the invalid class. As you can also see, this test has a lot going on. And the test for the double click with an invalid email repeats a lot of this code.

The Refactor

Instead of checking the first and second clicks in the same test, we can test the form when it has a clean slate, and then test the form when it already has the invalid class, like this:

describe('when the email input form is valid', function() {
describe('when the entered email is invalid', function(){
it('displays the invalid email input form view', function(done){
const shoppingModule = componentInDom();
registerEmailStub.returns(Promise.reject({ message: 'Invalid Email Address' }));
expect(shoppingModule.find('.shopping-email-submit').hasClass('invalid')).to.equal(false);
expect(shoppingModule.find('.shopping-email-input').hasClass('invalid')).to.equal(false);
shoppingModule.find('.shopping-email-submit').simulate('click');

defer(() => {
expect(shoppingModule.find('.shopping-email-submit').hasClass('invalid')).to.equal(true);
expect(shoppingModule.find('.shopping-email-input').hasClass('invalid')).to.equal(true);
done();
});
});
...describe('when the email input form is invalid', function(){
beforeEach(function(){
shoppingModule = componentInDom();
registerEmailStub.returns(Promise.reject({ message: 'Invalid Email Address' }));
shoppingModule.find('.shopping-email-submit').simulate('click');
});

describe('when you click the button to try again', function(){
it('removes the invalid form view and reverts to the standard view', function(done){
defer(() => {
expect(shoppingModule.find('.shopping-email-submit').hasClass('invalid')).to.equal(true);
expect(shoppingModule.find('.shopping-email-input').hasClass('invalid')).to.equal(true);
shoppingModule.find('.shopping-email-submit').simulate('click');
expect(shoppingModule.find('.shopping-email-submit').hasClass('invalid')).to.equal(false);
expect(shoppingModule.find('.shopping-email-input').hasClass('invalid')).to.equal(false);
done();
});
});
});
});

In the above refactor, the double click test is much easier to follow, since we’ve already simulated the first click in the beforeEach block that runs before each it statement. Plus, now the test file is better set up to take into consideration all user interactions, not just validation errors. Since I wrote the error validation functionality before working on the successful email sign-up functionality, I wasn’t originally thinking about how future tests would fit in. This way we can also test a successful email submission in the first describe block.

TL;DR

  • One of the trickiest parts of unit testing is isolating which functionality you actually want to focus on when testing. Use stubs as needed.
  • Make each individual test (the it statements here) as concise as possible
  • Group tests in describe blocks so the intended user flow is as clear as possible. Think about how existing and future tests fit in with the ones you’re adding.
  • When testing asynchronous functions in Mocha, try using defer and the done callback.

--

--