How to achieve 100% coverage of your UI components with the fewest unit tests

Image of Daniel Irvine
Daniel Irvine

One of the first decisions you need to make when testing the rendered output of a component is which parts of it to test. The straightforward TDD answer is obviously all of it, but since we like to be lazy we look for excuses to not do that. Here’s how I decide what to test and what to not.

In this post I’ll look at two ways to decide which tests to write: testing only behavior, and triangulating to cover branches.

Static vs behavior

Is behaviour involved? That’s the first question to ask yourself. If any of the rendered component is static data then I don’t always bother with a unit test. Some other kind of test is more appropriate. If I’m working with a QA then perhaps they care about that more than I do.

The reason for this is that a test for static data simply ends up repeating what’s in the production code itself.

Take a look at this component.

const PageHeader = ({ username }) => (
  <div id="header">
    <span>Welcome!</span>
    <span>You are logged in as <strong>{username}</strong>.</span>
  </div>
);

The first span, the “Welcome!” message, is not something I’m likely to test. Here’s what that test looks like. I don’t think it adds much except repeating what’s already said but in a more verbose way.

it("renders welcome message", () => {
  mount(<PageHeader username="Daniel" />);
  expect(container.textContent).toContain("Welcome!");
});

The second message is more interesting because it depends on the prop input. In this case I’d like to see that as a test.

it("renders the username", () => {
  mount(<PageHeader username="Daniel" />);
  expect(container.textContent).toContain(
    "You are logged in as Daniel"
  );
});

This test includes more information than what’s in the first test: it shows the link between the prop passed in and the output. This is a subtle difference.

If you’re following strict TDD then you can make that test pass simply by hardcoding.

const PageHeader = () => (
  <div id="header">
    <span>Welcome!</span>
    <span>You are logged in as <strong>Daniel</strong>.</span>
  </div>
);

In order to force the use of the prop, we need a second test. This is known as triangulation.

it("renders another username", () => {
  mount(<PageHeader username="Jack" />);
  expect(container.textContent).toContain(
    "You are logged in as Jack"
  );
});

Many experienced TDDists will skip this second triangulation step. I will generally opt to include it if I’m pairing or mobbing. That way you can keep yourselves honest, always writing the most minimal solution that ensures all tests pass.

Now what happens if we want to show a “Please log in” message if the username is null?

it("renders a login message if username is null", () => {
  mount(<PageHeader username={null} />);
  expect(container.textContent).toContain(
    "Please log in"
  );
});

Here’s the implementation that makes this pass.

const PageHeader = ({ username }) => (
  <div id="header">
    <span>Welcome!</span>
    { username
      ? <span>You are logged in as <strong>{username}</strong>.</span>
      : <span>Please log in</span> }
  </div>
);

However, this is not the simplest solution that makes this test pass. The following solution would also work.

const PageHeader = ({ username }) => (
  <div id="header">
    <span>Please log in</span>
    <span>Welcome!</span>
    <span>You are logged in as <strong>{username}</strong>.</span>
  </div>
);

Clearly this isn’t what we intended. But in order to arrive at the correct solution, we need to triangulate again.

it("does not render a login message if username is set", () => {
  mount(<PageHeader username="Daniel" />);
  expect(container.textContent).not.toContain(
    "Please log in"
  );
});

This test forces us to ensure we have the right implementation,.

Deciding to skip triangulation or not

I’ve shown two separate types of triangulation for behavior:

  1. To force the use of a prop rather than a hardcoded value
  2. To force the use of a conditional/ternary

The first case is the one in which you may want to skip triangulation. For the second case, I would never skip triangulation. The second case involves branching which means that if you’re aiming for full code coverage you need to test both branches—in other words, you need two tests. Contrast this with the first case where you’ll get full code coverage even with just one test.

What about styles?

You’ll also notice that I’ve avoided testing any styling or HTML tags. That’s not always the case; sometimes, the tag is important. For this component I’m more interested in the text that’s shown on screen. The span and div elements aren’t all that special--they are there for layout purposes only. Layout is something that is, generally speaking, static. Therefore it’s better tested within your system/acceptance tests.

However, there is a case for testing the strong tags. These have semantic meaning, not layout meaning. So you may want to encode that in unit test. Doing that is left as an exercise for the reader 🤣

Sign up to our new weekly newsletter and get free screencasts as a gift

Our weekly newsletter launches soon (we’re not quite ready yet). But you can sign up now. When it launches, we’ll send you 8 free screencasts showing how to go from 0 to 100 with test-driving React.