JavaScript Unit Testing for Humans

Harshal Patil
webf
Published in
5 min readApr 10, 2018

--

When it comes to writing an Unit Test for UI code, it is not difficult, but visualizing it as one isolated functionality is something that takes months, if not years, to master. Web UI is not just JavaScript.

Web UI = HTML + CSS + JavaScript

This makes it hard. Further, unit tests demand that you test components in isolation and that demand promotes that you design your entire system as a composition of loosely coupled components. It is hard to write unit tests for tightly coupled components, often creating more test code than actual source code as you have to stub/mock at many places.

At some point, every project has a hanging sword of timeline which compels developers to design things that are tightly coupled making things difficult to test in isolation. Add to that, there are several reasons why developers hate documentation:

But it is 2018. You love it or hate it, you write documentation or not, unit tests are here to stay. If writing documentation for source code is difficult, imagine the documentation for test code. It will practically be non-existent.

There are three problems here that we are trying to emphasize:

  1. Visualizing unit tests for UI components is wizard’s work (You have a shitty DOM to deal with)
  2. Documentation for test code is practically non-existent
  3. It is impossible to perfect design system upfront

Now, there is nothing we can do about our third problem, but we can surely do great if we follow one very simple guideline.

All we have to do is to add five comment markers and just good one-liner text for our each unit test we write.

Writing Good Unit Test

Writing good unit tests doesn’t means doing DRY, design patterns or extensive documentation. It simply means putting your unit test code into good structure.

It doesn’t matter what framework or library we use, 99.9% of the unit tests have simple 5-step workflow:

  1. Setup data
    Setting up required test data for testing the particular functionaliy (SUT). It is also referred to as test fixture in common literature.
  2. Setup spies/stubs/mocks
    Setting up mocks or stubs that we might need if component under Test is dependent on any external component/service.
  3. Execute SUT — System Under Test
    Actual component/function/method/block of code that we are interested in unit testing.
  4. Verify results
    After SUT is executed, verify the result by behaviour and/or data.
  5. Teardown
    Clear any stubs/mocks setup to clear memory.

These five steps literally translate into following code:

it('should be called with GET method', () => {    // 1. Setup data

// 2. Stub stubs/mocks
// 3. SUT - System Under Test // 4. Verify result (behavior / data) // 5. Teardown (cleanup stub)});

Simple unit test

We have a function findUser(userId) that accepts a userId, makes a fetch call and returns a Promise<User>. It involves asynchronous external API call that needs to be stubbed. To write unit tests, using the above structure, we can write our test as:

// Code snippet using Mocha + Chai + Sinon
it('findUser() should call fetch with GET method', () => {
// 1. Setup data
const promise = new Promise((resolve, reject) => {});
// 2. Setup stub
const stub = sinon.stub(window, 'fetch').returns(promise);
// 3. SUT - System Under Test
const result = findUser(userId);
// 4. Verify result
expect(stub.calledOnce).to.equal(true);
expect(stub.getCall(0).args[0].method).to.equal('GET');

// 5. Teardown (cleanup stub)
stub.restore();
});

Instantly our unit test becomes more approachable. It is very clear what is happening in this unit test.

Further, in another unit test, you might be interested in verifying actual result returned by the API which makes it asynchronous. We can write our unit test as:

// Code snippet using Mocha + Chai + Sinon
it('findUser() should return `User` for valid userId', (done) => {
// 1. Setup data
const promise = new Promise((resolve, reject) => {
resolve({ data: { id: 1, name: 'Harshal' }});
});
// 2. Setup stub
const stub = sinon.stub(window, 'fetch').returns(promise);
// 3. SUT - System Under Test
const result = findUser(userId);
result.then((user) => {
// 4. Verify result (asynchronous)
expect(user.userId).to.equal(1);
// 5. Teardown (cleanup stub)
stub.restore();

done();
});

});

Here our sequnce and nesting did change, but overall unit test is still easily readable. Mention of the word asynchronous to our comment makes it easy to indicate that we are testing some async code. For simplicity, we have used done callback to illustrate async behavior. We can further optimize our unit test to look more functional and sequential using promises, observables and async-await.

Often, we use same test fixtures and mocks for number of unit tests. For this, we often move setup data and stub to beforeEach block. In this case, our sequnce changes but we still end up with clean structure:

// Code snippet using Mocha + Chai + Sinon
beforeEach(() => {
// 1. Setup data
const promise = new Promise((resolve, reject) => {
resolve({ data: { id: 1, name: 'Harshal' }});
});
// 2. Setup stub
const stub = sinon.stub(window, 'fetch').returns(promise);
});
it('findUser() should return `User` for valid userId', (done) => { // (1) and (2) - Before Each // 3. SUT - System Under Test
const result = findUser(userId);
result.then((user) => {
// 4. Verify result (asynchronous)
expect(user.userId).to.equal(1);
// 5. Teardown (cleanup stub)
stub.restore();
done();
});

});
it('another test', () => {
// ...some more code
});

Of course, it is not necessary that you should have all the five steps. If you don’t need to mock something, then step 2 and step 5 will be optional. You can simply write N/A comment:

// 5. Teardown - N/A

Few things to remember

  1. Step number is mandatory
    It is important that you put step number for each comment. Step number serves as a anchor for your test around which your logic flows
  2. Not all steps are necessary
    Only step 3 and step 4 are mandatory. Rest of them are optional. But still include comment for optional step.
  3. Be explicit about verification step
    Explicitly state if you are verifying the behavior or data. Also state if you are verifying asynchronous or synchronous code. Just add it to comment as we did earlier. Understanding difference between verifying the behavior and verifying the data is topic for another day.

Writing effective unit tests is a fun activity. Well written unit tests reduce the complexity of the test code and it further helps developer understand source code better with most of the corner case scenarios covered by unit tests.

--

--

User Interfaces, Fanatic Functional, Writer and Obsessed with Readable Code, In love with ML and LISP… but writing JavaScript day-in-day-out.