Tutorial: Build a button using test driven development
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"
.
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:
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.
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!
Tip: You can check what gets rendered by calling screen.debug
in the test:
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
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!
Wrapping up
Congrats on doing test driven development!
How did you find this exercise? Let me know in the comment below.
Member discussion