Testing burns calories

May 23, 2022

Profile picture

Brought to you Dave Cooper, a web developer in Toronto, Canada who has been building internet things for quite a while now. Say hello on Twitter.

I think a lot about how developers spend their energy. With any given feature, there are things around the edges that require a developer’s attention and energy. Architecture. Performace. Scalabilty. And also more mundane things like coding style, naming conventions, and semi-colons. The list goes on. It all takes energy. It burns cognitive calories.

When it comes to automated testing, it’s easy to waste a lot of energy on tests that don’t add a lot of value. But with a slightly different approach, your tests can provide real business benefit, and also give other developers a deeper understanding of your product’s core features.

What you test matters to the whole team, and the whole business

Some teams focus on hitting a very high percentage of test coverage. Historically, some teams have found creative ways to avoid tests completely. Both extremes seem wrong to me. Instead, I like to burn calories on writing a few good tests, for the core features that have real business value.

Let me give an example. A colleague just picked up a difficult bug in a pricing-related feature. Prices in this app are in the thousands of dollars — we’re not selling sunglasses here, folks — so here we have a great opportunity to write really valuable tests.

We realize that the current implementation is difficult to test without mocking APIs, cookies, application state, and so on. The tests are hard to write for the same reason the bug has appeared — a poor separation of concerns. We talk about refactoring in a way that breaks up one monolithic component into a few completely dumb components, with the critical business logic lifted up and centralized in a consuming component.

We write snapshots tests for the dummies, to ensure each one has the DOM we expect given a certain set of props. This only takes 20 minutes. Then we spend the better part of the day writing specific and expressive tests for the important stuff.

Now that we’re done, a critically important feature is more stable and maintainable. It’s easy to think about. It has concise and crystal-clear test descriptions and assertions. The tests map cleanly to the original user stories and requirements. But I think one of the best outcomes will come later: the next developer to dive into this feature will have a roadmap, and can code with confidence.

How you describe your tests adds clarity and reduces cognitive load

We should test in a way that resembles the way our software is used, instead of its implementation details. Kent C. Dodds has explained this well. But he skips over a valuable bit: over time, well-written and user-focused tests become an easy-to-digest repository of team knowledge.

Consider the following example of a test, from the article I linked to, that pays attention to implementation details. This is the description and one of the assertions:

test('setOpenIndex sets the open index state properly', () => {
  /// ...
  expect(wrapper.state('openIndex')).toBe(1)
});

This test is problematic not only because it’s testing the internal implementation of a feature. It’s also making me, the developer, guess what the test is about. I can sort of guess that the “open index” refers to which accordion item is open. But I can’t possibly guess what “properly” means until I reverse-engineer the test in my mind (for example, I see that the test expects some state value to be a particular integer, so that must be the “proper” one, right?) Tests like these add unnecessary cognitive load to a developer’s work. Not a useful way to burn calories.

Now let’s look at the new-and-improved test. Again, just the description and a couple of the assertions:

test('can open accordion items to see the contents', () => {
  ///...
  expect(screen.getByText(footware.contents)).toBeInTheDocument()
  expect(screen.queryByText(hats.contents)).not.toBeInTheDocument()
});

See what I mean? Sure, these tests are more reliable. But they also require less thinking. The human-written parts, and even the idiomatic method names from using the right tools (e.g. React Testing Library), are so much easier to reason about. I’ll be the first to admit that these kinds of tests can take a little longer to design, because you have to mentally “zoom out” from the internals and look at the feature from the user’s perspective. But the results make it totally worth the burn.

(By the way, I highly recommend using the calorie-burning metaphor around the office. People get it right away, and you get fewer eye-rolls than when you use the latest productivity buzzwords.)

Happy testing! Feel the burn!

Photo by sporlab on Unsplash