Why is unit testing important?
Unit testing has fallen out of fashion within the front-end community. After half a century of structured programming, and twenty years of XP, our industry still struggles to benefit from unit testing. Why is this the case?
If you’re sceptical about unit testing, please stick with me as I take this opportunity to defend the practice.
Here’s some examples of the kind of comments I’ve heard from front-end developers:
- Always prefer system tests to unit tests
- Don’t bother with 100% code coverage as it’s a futile pursuit
- Mocking is a code smell
- Following TDD on the front-end is counterproductive
- Popular software design literature is wrong
- Design literature is correct but it doesn’t apply to the front-end
- <Continuous deployment | snapshot testing | generative testing | some other technique> makes unit testing redundant
- You shouldn’t test implementation details
And yet, there’s an enormous number of people (don’t ask me how many) who have successfully applied unit testing to improve the quality of their software and their own effectiveness as developers.
Are they wrong to have faith in unit testing?
Nope. They just had more success with the technique than others.
Unit testing ain’t easy, but just because you struggled doesn’t mean it’s a broken technique
If you believe any of the statements above, then your belief is likely based on your own personal experience–unless you’re simply parroting what you heard someone else say, in which case, hopefully I can sway you just as easily.
It’s a perfectly natural—and completely understandable—reaction for any human being to dislike anything that has burned them. It boils down to cognitive dissonance. You struggled with a testing technique, like mocking. It didn’t work out for you. But you also believe yourself to be a great programmer who should have no problem with anything that others do. These two things are in competition with each other. Either testing is broken, or you haven’t quite mastered it yet. Which is right?
Your ego doesn’t like to be bruised. So rather than letting you accept that perhaps you just need more practice, it will tell you that “It’s the technique’s fault! It’s to blame!” It can’t possibly be that you just tripped up and made a mess of it. All those other programmers that came before you, all those software design and testing books, are wrong. Dead wrong. Unit testing can never work!
Is this you?
You have to get past this emotional response. Do not let your ego block off valuable techniques! It will stunt your development!
Rather, accept that unit testing is hard and takes a great deal of effort to figure out.
Most of us are writing terrible tests most of the time.
“I inherited a bunch of unit tests and they were a mess! They made my life hell!”
I repeat, unit testing ain’t easy. It takes years and years of practice to do even a moderately “good” job, and even then it’s very hard to judge what makes a test “good” or not.
Mocking is the prime example. It’s so easy to get lost in a tangle of mocked dependencies.
Many people, make awful testing mistakes when they first practice mocking in unit tests. They end up mocking the world and their tests become long (take forever to read), cryptic (hard to understand) and brittle (break a disproportionate amount of time).
A bad experience with mocking is where many, many people fall out of the unit testing boat and actively begin to start being “anti-“ unit tests.
But mocks can be wonderful, and are a crucial tool, when used correctly.
There are some great books out there that teach you how to mock well. You just have to read them.
The gradient from “good” to “bad”
We have, as an industry, developed some ideas of what makes a “good” unit test: generally speaking, it follows Arrange-Act-Assert; it’s just a few lines long; it’s fast to run; and so on.
No one sets out to write a “bad” test. There are many, many trade-offs involved in each and every test we write. Very often we don’t realise we’ve made wrong choices until we’re much further down the path. That’s why we learn to refactor our tests, and constantly ask our tests to prove their value.
If a test is screaming at you—perhaps it’s always breaking or perhaps it’s never clear about why it’s failing—then refactor it.
The front-end is not special
Perhaps the main reason people feel that the front-end is special is because so much of front-end development is declarative HTML or CSS, and therefore tests are simply repeating what is already on the page. I agree with this. But that’s not special, it’s just a bonus that declarative programming is so readily available on the front-end. You don’t have to write unit tests for that code.
When I’m coding the back-end, I look for opportunities to use static data to encode behaviour. Sometimes that means selecting a more complicated algorithm than I would have otherwise chosen, but it means I can avoid unit tests for whatever data that algorithm uses. Often, with the right abstraction the code ends up simpler.
But when it comes to behaviour? Well, I still feel duty-bound to write unit tests for every single bit of it.
Your unit tests should always be about behaviour, not static data
Take, for example, this little snippet of a React component:
<button type="button" disabled={disabled} ... />
The disabled
attribute is not just data. It specifies behaviour. In some scenarios, this button is enabled, and in some it is disabled. What are those scenarios? That is what your tests should be testing. You need to have at least one test for the enabled case and one for the disabled case, and more depending:
it("submit the form when the form is filled out correctly")
it("disables the submit button when form validation fails")
it("disables the submit button when the form is submitting")
“But I can test the same thing with a system test!”
Beyond unit tests lie types of testing like integration, system and acceptance testing. These tests have a larger surface area—they instrument a great amount of code. Not only does that mean they take longer to run, but it also means they will break in more circumstances and it takes longer to figure out why something broke.
Compare that with unit tests. A good unit test should quickly point you to the issue. To take the example of the disabled
attribute above. If I accidentally delete the attribute, I expect one of my unit tests to break, the one that says “disables the button when the form validation fails”, or whatever the test is. Even the test description is enough to tell me where the issue is.
Remember the testing pyramid? There is a reason why you should have an abundance of unit tests versus integration tests or system tests. This advice does not change for front-end.
Yes, testing components is not fun
Components are hard because they involve the DOM, combined with composition of other components. We still have to test components if they have behaviour, but you can make your life easier by getting as much behaviour as possible out of your components.
Make sure you are not at the mercy of the framework. This advice is the same for any language, any framework. Plain ol’ JavaScript objects are much easier to test than components.
Redux is also easier to test than React components. Why not try to write a Redux application with a React front-end, rather than a React application with Redux stores?
The strawman argument of “implementation details”
Structured programming is, and always has been, about finding logical units that neatly encapsulate behaviour and data together in a re-usable package: a “unit”.
Conveniently enough, it is these units that lend themselves well to testing. You can argue either way whether you’re testing implementation details or not. But it’s a strawman argument. It doesn’t matter. What matters is whether writing those tests gives you value. Some units will benefit from having tests, some won’t.
It can take experience to know which units are worth testing and which aren’t, but it’s self-defeating to start from a position of “avoid unit testing at all costs.”
Unit testing isn’t not important
What I’ve argued in this post isn’t so much why unit testing is important, rather it’s why it isn’t not important.
In future posts I’ll focus in on each of the benefits that unit testing brings.
— Written by Daniel Irvine on January 8, 2020.