Getting Started with JavaScript Unit Testing
Do you write unit tests for your code? I bet you don’t. Here is why.
Most software developers have heard at some point that unit tests are a Good Thing. I’m sure lots of us even believe that to be true. A few months ago I asked readers of this blog to share how they approach JavaScript unit testing. The poll yielded following results:
Answer | Percentage of responses |
---|---|
I don’t know what are unit tests | 2.7% |
My code doesn’t need unit tests | 5.3% |
I don’t write tests, but I would like to | 48.0% |
I write tests occasionally | 18.7% |
Writing unit tests is a part of my normal workflow | 25.3% |
A while ago somebody else asked the readers of Hacker News similar question, this time not limiting it to a particular language. The results made it clear that majority of developers don’t practice unit testing, let alone Test Driven Development.
Only a few respondents discarded tests as worthless. There seems to be a consensus that testing is good. But so is with physical exercise and eating wholesome food. We know it’s good for us. Yet most of us just don’t do it.
Recently I have been advocating testing in a few talks I did and in the team I joined recently. But I was also advocating testing to myself. Unit testing is not entrenched in my DNA. Even though I’ve been using JavaScript for about 10 years now, I started to write unit tests just over two years ago, thanks to some great people who have taught me this skill. It’s certainly not easy and after two years I still learn how to do it better. My natural urge is usually to skip tests when I write code and I understand if you feel the same.
Then why even bother?
Despite initial difficulty of writing tests I have found over and over that each time I write code with tests the final result is more elegant, clean and bug-free. The result is not better just because I now have a test demonstrating my code works. Programming with tests forces you to design the code to be reusable, loosely coupled and have a clean API. In other words, it makes you write good code.
Complex code full of methods that share a lot of state variables is difficult to test. The tests will require lots of preparatory setup and the knowledge about internal structure of your methods. On the other hand, methods that simply accept input and return output are a breeze to test. It’s not a coincidence that such methods are also easier to read, understand and re-use.
Writing tests before the implementation is known as Test Driven Development (TDD). It has another benefit: it requires you to think about the API before you start implementing it. When you first write client code (in this case: a test), you think from the perspective of the consumer. You need to make your API readable – a worthy goal that often gets lost in a train of thought fueled by rushing toward implementation first.
I don’t have time for this!
The main argument I have heard against unit tests is that they take time. “We have to ship NOW and we don’t have time for such frivolities at the moment. Maybe later, when we’ll have time to catch a breath.” This excuse is questionable at three different levels:
- This statement makes an assumption that tests are separate from the actual code and something on top of the implementation. If that assumption was true, I would agree that writing tests is a waste of time. But tests should be a part of delivery. You write them as you go to verify that individual methods do what you expect them to do. Use tests to establish if you can start writing the next method, because the first one really does what it should.
- At least in the universe where I’ve been writing code, “catching a breath” always becomes a victim of new features and ever-too-short deadlines.
- There is little to be gained from writing tests retrospectively, after finishing the implementation. It’s an unrewarding task. Even if you find bugs in old code while doing this, it’s always more boring than creating something new and getting instant feedback from passing tests.
So does writing tests really make you slower? Intuition suggests otherwise: after all, you have to write more code. In my own experience I was usually punished whenever I did not write tests.
For example, a few weeks ago I was working on a small feature (roughly 200 hundreds lines of JavaScript) that had a few tricky corner cases. I quietly skipped unit tests and verified that the code works by running it in the live application. Few days later somebody found a serious bug and I was tasked with fixing it. After a day of tedious debugging I found that the problem originated in a loop that was running one time less than it should. This is a textbook example of a silly mistake that could be easily detected by a unit test. In other words, saving one hour on tests resulted in a day wasted on debugging later on.
This story is typical. I noticed that with tests I often discover bugs and edge cases that would have escaped notice if I were only verifying the final product in a browser. Most of my bug hunting ends up happening in the code that has not seen the light of a test.
In that regard writing tests works like changing tires during a Formula 1 race. A driver can skip a tire change and get a short-term advantage, but eventually he will lose it to other drivers with better tires – if he completes the race at all.
Tests may require extra time in the first hours of working on a new feature, but they pay off very quickly. You save time on bugs that get caught early on. And you get instant feedback without having to open up the full application in a browser.
That doesn’t mean that tested code is 100% bug-free. Errors may slip through tests, though that is rare and typically results from incomplete specs or insufficient test coverage. Tests provide a safety net, ensuring that any changes you make will not break existing functionality. Good tests will notify you whenever you make an incompatible change – they’ll not pass until you fix them.
How to get started?
I recommend talking to your teammates and asking whether any of them are writing tests already. The fastest way to learn how to write tests is pair programming with somebody who already has the experience.
When you are on your own, take a look at popular JavaScript libraries. Most of them have good test coverage and you can learn a lot from reading these tests, for instance from jQuery.
What does it take to write a “good test”? Practice. The best thing that can happen to you is to have a mentor – somebody experienced with unit testing who will help you with your first steps. I was lucky to learn from such a person. If you have an opportunity, participate in Code Retreat. It’s an excellent way to learn from other people who passionate about software craftsmanship. There are online resources too. Stack Overflow has a page that is a good starting point, but it is always good to have somebody else to review your tests. With practice you’ll learn how to avoid useless and bloated tests (common mistake in the beginning) and improve your productivity.
There are dozens of test frameworks that you can use. Too much choice can be more confusing than helpful, so I have a recommendation for you: download buster.js. It’s relatively new framework that has several wonderful features, a few of them unique:
- It comes with a test runner. The runner allows you to execute your tests in many browsers with one command, without having to manually go to each browser and reload the page. It’s the fastest way to do cross-browser testing.
- It works with both node.js and web browsers.
- Because buster is written in JavaScript, you can easily extend it with assertions and even full-blown features.
- It comes with sinon.js (in fact buster and sinon are written by the same person, Christian Johansen). Sinon is a helper library for more advanced tasks you will most likely have to deal with eventually: mocks, stubs and asynchronous tests.
- Buster is actively developed and has good documentation to get you started.
How to actually write your first tests if you have never done it before? A unit test in JavaScript is simply a function that tests another function.
When you’re about to write a new function, ask yourself “How do I know that it did what I expected it to do?”. When you answer this question, you’ll have a recipe for a unit test. Let’s look at few examples:
Basic function
Your function has to accept some input and return a different value. Thus your first test should call this function with sample parameters and check whether it returns expected value. For instance, if you have a factorial
function, your test may look like this:
"test factorial for a small integer": function() {
assert.equals(factorial(5), 120);
}
A common convention is to start name of each test method with the word test
, although buster.js allows you to to use any method name you want.
DOM manipulation
Your function modifies the DOM: creates a new element, changes an attribute etc. Your test function should check if calling the function will yield the expected change. In this example setHeader
should modify the content of the first h1
element on the page, so you may write a test similar to this one:
"test setHeader modifies h1 header": function() {
var header = document.querySelector("h1");
assert.equals(header.innerHTML, "");
setHeader("Hello");
header = document.querySelector("h1");
assert.equals(header.innerHTML, "Hello");
}
You may find people who say that operations involving the DOM should not be unit tested, since the DOM is external to your code and tests should not verify external systems. While there’s some merit to that, such tests are useful to find mistakes in your code and highly unlikely to fail because of bugs in a browser’s DOM implementation.
Ajax
Your function sends an HTTP request and expects particular answer from the server. Testing that is more complex, because the code is asynchronous and uses the network. The general rule here is that you should NOT test your internet connection and not send real requests. Instead create a mock for the code communicating with the server and verify that it receives expected values. sinon.js documentation comes with detailed examples.
Pitfalls
Each of the verified functions above did just one thing and thus was easy to test. Real code tends to be much more complex, functions grow big and rarely do one thing. But these are exactly the symptoms of bad style that unit testing can help you with. One of the great things about testing is that it forces you to write better code. Bad code is difficult to test and making it testable will make it cleaner.
Below are typical code smells that make programs more convoluted and harder to test:
- Methods that do more than one thing. Such methods are not really units. They’re both difficult to read and to test, because apart from their main, expected behavior, they trigger incidental side effects. Solution: keep your methods short (preferably under 10 lines) and if they start to grow too much, split them into smaller methods with their own tests.
- Methods from one class/module share too many state variables. It makes tests harder to write because you’ll have to setup the initial state for your methods before you can call them. That may require a lot of preparatory code. Solution: when possible, write your methods in the functional style, so they accept all the necessary data as input parameters and return the results instead of introducing shared state variables affecting other methods.
- Private functions that can’t be accessed by tests. This is a tricky problem. Encapsulating functions by making them private is not really a code smell – in fact it’s good. In theory your modules should expose only the necessary functionality via public API and keep the rest private. Unfortunately private functions can’t be accessed in unit tests. My recommendation is to expose them via prototype or the module pattern. In this case I prefer to trade encapsulation for testability.
- Methods have a lot of external dependencies. Of course it’s impossible to avoid libraries or modules. They make unit testing more challenging, because you either have to include the external libraries (making tests more complex and brittle) or mock them. Solution: use dependency injection and pass dependencies as parameters to your your methods. This way in your tests you can substitute the real dependency (e.g. a large library) with a small mock. Using a mock ensures that you test only YOUR code and not the library.
- Your constructor functions are long and hold complex logic. When a constructor function does a lot, it makes it harder to set up new objects in the test. Solution: keep initialization code of constructor functions short and consider moving the logic to separate methods that can be called later. As a workaround, when you have to test a method of a class that has a complex constructor, you may be able to access such method via prototype and invoke it with
call
orapply
. This is only possible if the method doesn’t depend on the state initialized in the constructor.
What’s next? Sit down and write some tests! The next time you have to implement something new, think how you can verify that your code works the way it should and write it down as a test.
I will be honest with you: writing tests is not always easy and it takes effort. However, as you learn it, you will realize that it is often the fastest way to ensure bug-free and clean code. It will change the way you think about the structure of your applications and will make you a better programmer.
And remember: having some tests is always better than having no tests.
Comments are closed