Explain JavaScript unit testing like I’m five

Corey Cleary - Nov 20 '18 - - 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, links to great tutorials by other developers, and other freebies!

Unit testing is so critical to good software development, yet for beginners (and many experienced professionals as well) it's something that can feel alien and uncomfortable at first. It might be something you know you should be doing, but haven't had time to learn, or tried to and didn't get very far. It might also be something you've never even heard of before.

Especially when you're a new JavaScript or Node developer and you have a million other things to learn, it can be easy to "offload" it to make more space for the mental computing power necessary for what you're currently focused on.

Whether you're at the point in your learning that you feel you can pick up unit tests or not, having a good high-level understanding of what they are and what purpose they serve will help you both now as well as in the immediate future.

ELI5 (explain like I'm five)

At a high level, unit tests are tests that prove your code is working as expected. They're like a "safety net." Remember when you had to do proofs in math class? They're kind of like that. [Side note: there are other perhaps better analogies, with things called formal methods, but don't worry about that for now]

What happens when you're working on an application and you want to make a change to existing code? Will this break the application? How do you know?

How do you know why code was written in the first place? What are the system or business requirements?

This is the purpose that unit tests serve. If you do make a change to a function, and there are tests already written that are good enough to cover that change, you should be able to make the change with confidence. They should also serve as a form of documentation for your application, somewhere you can go to to read and figure out what the app is intended to do when you can't figure this out from the code.

Understanding the structure

All projects differ, but many follow the folder structure of putting the code in src/ and the tests in test/ (or tests/). I'll link one of my own GitHub repos here to demonstrate, since it's my code I know it well. This repo is very simple in terms of structure too, so that also makes it easy to demonstrate unit tests.

In the tests/ folder there is one test file (a *.spec.js file) that "maps" to each of the files in src/. Sometimes the test file might be in the format of *.test.js or something-test.js, but this is just formatting. What's important is they all follow the same format. And again, all projects are structured differently but you'll usually find one test file per JS file.

Let's take a look at one of these tests:

const compose = require('../src/compose')
const expect = require('chai').expect

describe('COMPOSE', () => {
  it('should compose a function from right to left', () => {
    const minus2 = num => num - 2
    const times2 = num => num * 2
    const result = compose(times2, minus2)(4)
    expect(result).to.not.equal(6)
    expect(result).to.equal(4)
  })
  it('should compose a function with one function', () => {
    const minus2 = num => num - 2
    const result = compose(minus2)(4)
    expect(result).to.equal(2)
  })
  it('should compose a function with more than one function', () => {
    const minus1 = num => num - 1
    const times2 = num => num * 2
    const result = compose(times2, minus1)(4)
    expect(result).to.equal(6)
  })
  it('rightmost function should be variadic (accept more than one argument)', () => {
    const addGreetings = (greeting1, greeting2) => greeting1 + ' ' + greeting2
    const sayItLoud = greeting => greeting.toUpperCase()
    const oneArgProvided = compose(sayItLoud, addGreetings)('hi')
    const allArgsProvided = compose(sayItLoud, addGreetings)('hi', 'there')
    expect(oneArgProvided).to.equal('HI UNDEFINED')
    expect(allArgsProvided).to.equal('HI THERE')
  })
  it('all other functions besides rightmost should be unary (accept only one argument)', () => {
    const addGreetings = (greeting1, greeting2) => greeting1 + ' ' + greeting2
    const addMoreGreetings = (addedGreetings, addtlGreeting) => addedGreetings + ' ' + addtlGreeting
    const allArgsProvided = compose(addMoreGreetings, addGreetings)('hi', 'there', 'tests')
    expect(allArgsProvided).to.equal('hi there undefined')
  })
})

You can see that in the compose.spec.js test at the top of the file you'll import the code you want to write tests for:

const compose = require('../src/compose')

And then in the body of the test file, you'll find a describe() which can be though of as a grouping of tests, followed by a bunch of it()'s which are the unit tests themselves (called "assertions"), i.e.:

it('should compose a function from right to left', () => { etc....}
it('should compose a function with one function', () => { etc...}

and so on.

This describe (grouping of tests) -> it (assertion for specific unit) pattern is, for the most part, what you will find in JavaScript unit testing.

These tests declare what the compose module should do under a set of given circumstances, which you as the developer come up with. There are guiding principles for things to test like - it should accept the right type of argument, should return something if it's supposed to, etc. - but a lot of this will depend on the application and how that function is supposed to behave within the application. This is the point where you use your best discretion for figuring out how they should be written, and this develops best with practice and understanding requirements.

Which brings me to one more important point:

Unit tests test things at the unit level, meaning the smallest piece of functionality that makes sense to test for. Unit tests will not test something like: "user should be able to click a button, which should call a service, which should register the user, which should return a success message to the user." This would be considered an end-to-end test, and if you find yourself writing unit tests like this you need to break them down much further. You could break that end-to-end test down by each "should" and that would be closer to unit tests, depending on the code of course.

Tip: a good general rule of thumb is to have a unit test for each public function in your codebase.

Lastly, there is a lot of chatter in programming circles about what code coverage level should be.

While you're first learning unit testing this is not something to be concerned with at all, and even when you're comfortable with writing tests, the "what level of coverage you should have" discussion can be misleading.

Misleading because it's often the wrong metric. You should be writing good tests, not hitting some arbitrary metric that is often used by management as a "check the box" metric.

But what are good tests?

Good tests are ones that others can read and figure out why you wrote something the way you did, what the application requirements are, and should break with breaking code. This is a basic heuristic - that can be added to, of course, depending on your particular application/team/scenario, etc.

Where to go from here

You don't have to start writing unit tests today to be able to take advantage of them. Having the knowledge of what they are and how to navigate them in your project structure, you can start poking around and looking at what tests are currently there.

You can make small changes to the code under test, run the unit tests (usually this is npm test in your project), and see what happens.

And when you feel like you're starting to get the hang of it, you can start by adding a small test for a piece of code you wrote, and go from there.

While unit testing can appear difficult to get started with, this is often a mirage. Start small and work your way up. You can start adding small tests within a week I'm sure. And the benefits you get out of it will be immense - documentation for your code and a safety net for making changes.

I'm writing a lot of new content to help make testing in JavaScript (and JavaScript in general) easier. Easier, because I don't think it needs to be as complex as it is sometimes. If you don't want to miss out on one of these new posts, here's that link again to subscribe to my newsletter!

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