4 min read

Tutorial: Build a button using test driven development

💡
This is a tutorial that solves the Build a button using test driven development exercise.
If you haven't already, try to solve it yourself first before reading the solution!

Introduction

The first step in tackling this exercise is to just run the tests.

You can do that by running npm run test.
By default, it will go into watch mode and rerun the tests everytime a file is changed.

Looks like all tests are failing, so let's try to fix each one by one.

Test 1: The Button component should render an actual HTML button element

Let's take a look at the test failure: it says Unable to find an accessible element with the role "button".

Screenshot2023-02-14at070923

Our test is looking for a button in the page and doesn't find one (and that's expected, the Button component doesn't do anything yet!).

The error message also shows the DOM that gets rendered, and you can see it's just an empty body and div (shown in blue):

    <body>
      <div />
    </body>

Let's update the Button.jsx file to return an HTML button element and see if the test passes:

// src/Button.jsx
const Button = () => {
  return <button />;
};

And it does, the first test now passes:

Screenshot2023-02-14at072227

Test 2: The Button should allow passing in the button text

Next, let's also pass some text for the button - as now it's just empty.

Screenshot2023-02-14at072548

Let's check in the test how the Button will be used:

// src/__tests/Button.test.jsx
render(<Button text="Click me" />);

So the text needs to be passed as a prop.

Let's add a text prop to the Button and use it as the button value:

// src/Button.jsx
const Button = ({ text }) => {
  return <button>{text}</button>;
};

Does the test now pass? Yes!

Screenshot2023-02-14at073512

Tip: You can check what gets rendered by calling screen.debug in the test:

Screenshot2023-02-14at073628

Test 3: The Button should allow passing a click handler

Reading this test introduces a few new concepts, so let's go over them one by one. Feel free to skip to the test fix if you're already familiar with them.

Working with mocks

    // src/__tests/Button.test.jsx
    const dummyClickHandler = jest.fn();
    render(<Button text="Click me" onClick={dummyClickHandler} />);

jest.fn() is a Jest mock function. It's useful to pass a mock function instead of just a plain function as our click handler because it's then easier to check if was called or not.

For example, we can use expect(someMockFunction).toHaveBeenCalled() to check that a function was indeed called.

And we can even debug it using console.log(someMockFunction.mock.calls), which will output all calls of that function, and the parameters it was called with each time.

Simulating user interactions

    // src/__tests/Button.test.jsx
    import userEvent from "@testing-library/user-event";
    ...
    const user = userEvent.setup();
    await user.click(screen.getByText(/Click me/));

To simulate a user clicking or typing, React Testing Library provides the user-event library.

This allows us to interact with a component like a user would and encapsulates a lot of the complexities of DOM events behind the scenes.

The exercise uses user-event v14, which introduces a new API for events, that requires us to first setup the context (userEvent.setup()) and only then interact. The click method is now asynchronous, which means we need to wait for it to complete (using await), before we can do our assertions.

Fixing the error

Screenshot2023-02-14at075550

The error says that the test was expecting more than 1 call (of dummyClickHandler), but received 0 calls to the mock method - and indeed, that makes sense, our component doesn't accept an onClick handler yet. So let's add it!

// src/Button.jsx
const Button = ({ text, onClick }) => {
  return <button onClick={onClick}>{text}</button>;
};

And it now passes, wohoo!

Screenshot2023-02-14at075950

Wrapping up

Congrats on doing test driven development!

How did you find this exercise? Let me know in the comment below.