We write a lot of unit tests at Hubstaff. Testing is something we have taken seriously from day one, and as a result it’s a big part of our development culture. If you’ve been on the fence about writing unit tests but haven’t dove in yet, let me tell you why you should. You’ll save yourself a bunch of pain and work happier overall. Plus, happy programmers write better code. And there’s no better medicine for a software product than a frequent dose of high quality code.
Myths About Unit Tests
Most developers don’t write tests, and it’s easy to understand why. Tests take time (and in most cases money) to write. Maybe your boss or client thinks that your progress will slow down. Maybe your company doesn’t think the cost is justified. Unit tests do increase the size of the code base, which means more lines of code to maintain.
So you skip writing unit tests because you think that you’ll save your company money, finish your work early and make it home in time to watch “Game of Thrones.” However, give me a chance to change your mind.
To quickly cover what unit tests are, this is an RSpec test, but most unit test frameworks look and work similarly. These tests assert certain conditions. A test does X and expects Y. If the test does not get Y, it fails. It’s that simple.
3 Reasons You Should Write Unit Tests
You Can Find Out If Your Code Really Works
Unit tests aren’t a replacement for functional testing, but they are the solid foundation on which the rest of your testing process should be built.
Opening your app and taking new features for a test drive is a good practice. This is a standard part of our testing process at Hubstaff. We don’t ship code if it hasn’t been functionally tested. I’d also recommend getting one or two other people to open the app and exercise the new feature too. But this sort of functional testing is far from ideal.
Functional testing is an imperfect way to test new code for several reasons:
- Usually you (the developer who wrote the code) performs the functional testing. You’re going to use the feature as expected since you designed it. But software usually breaks when you use it in unintended ways.
- Functional testing is tedious. It requires you to think up ways to test the feature. If you are not the developer, you probably have no clue where the fragility in the feature is. I’ll often email the people who will be performing the functional testing a list of things to check because I know what code I changed and what it’s likely to affect. That’s exactly what writing unit tests is all about.
- It is usually unscripted. So if you ever do regression testing, this feature will be tested differently and almost certainly not as thoroughly the second time around. Conducting functional regression testing isn’t reasonable or a good use of time.
Without unit tests, you really don’t have a testing process. You have testing chaos. That chaos will result in a lot more time spent than it takes to write unit tests.
It Will Save You a Lot of Pain (and Time)
I don’t like interruptions while I’m working. Who does? So I’ll do just about anything to avoid them. One of the worst types of interruptions is when something breaks in production and I have to stop what I’m doing to put the fire out. Unit tests are one of the best ways to prevent this. You look at your code through a different lens when you are writing tests. This causes you to see the fragile parts a lot easier. You’re much more likely to catch errors that you would have missed before shipping the code to production if you are taking the time to examine it. I have stopped spending my time putting out fires and can focus almost all of my time on forward progress.
The unit tests are really just the documentation of how your code should behave. Some people will tell you that this documentation is valuable and I would agree. If you cannot tell from looking at the code what it is trying to do — which is a problem in itself — at least you have the unit tests to tell you the story. This is especially helpful if you are working with someone else’s code. But even if it’s your own, how well will you remember all the edge cases in six months?
Another often overlooked advantage of unit tests is being able to work through a problem faster. How many times have you received an error message when you test a feature in the app? Think about how much time it takes to have to launch the app, try something, see that it breaks, look at the code, rinse and repeat. It’s much easier to just write a test that causes the error and tests for the successful condition. Then change your code and rerun the test. Your development cycle will improve by an order of magnitude.
It Makes Deployments a Snap
I used to: run tests on my feature branch, pull down the latest changes from master on GitHub, merge the feature branch into master, run tests on master, push code to a remote repository and run database migrations.
There are all sorts of things that can go wrong in this process. I could easily make a mistake and forget to run the tests on master after I merge in my feature branch. I would often forget to run the database migrations after deploying the code to production. Life was not good.
Then I discovered continuous integration. Since we prefer using fully-managed, hosted solutions, we use CircleCI. We have found that CircleCI has the best grasp on the technical issues that need to be tackled with any continuous integration service. My six step process above was reduced to one.
With CircleCI, all I have to do is review the pull request on GitHub and merge it since CircleCI integrates with GitHub. It previously runs that branch when I push it to GitHub and tells me if they all passed. Once the merge occurs, it runs all the tests on the new version of master. If those pass it pushes the code to staging and automatically runs the migrations.
To push code to production, all I have to do is merge master into production. It takes care of the rest. If a test happens to fail, the deployment halts. Life is good.
What to Do If You Already Have Thousands of Lines of Code
If you’re convinced but already have thousands of lines of code in your code base, I recommend the following strategy:
- Don’t ship any new features without writing unit tests for them first.
- Write a few tests for existing code with each new feature.
- If something breaks in production, write a test for it at the time that you fix it.
Little by little you’ll increase your code coverage without grinding development to a halt.
A version of this post originally appeared on the Hubstaff blog.