Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Mastering React Test-Driven Development

You're reading from   Mastering React Test-Driven Development Build rock-solid, well-tested web apps with React, Redux and GraphQL

Arrow left icon
Product type Paperback
Published in May 2019
Publisher Packt
ISBN-13 9781789133417
Length 496 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Daniel Irvine Daniel Irvine
Author Profile Icon Daniel Irvine
Daniel Irvine
Arrow right icon
View More author details
Toc

Table of Contents (20) Chapters Close

Preface
Who this book is for
What this book covers
To get the most out of this book
Get in touch
1. First Steps with Test-Driven Development FREE CHAPTER 2. Test-driving Data Input with React 3. Exploring Test Doubles 4. Creating a User Interface 5. Humanizing Forms 6. Filtering and Searching Data 7. Test-driving React Router 8. Test-driving Redux 9. Test-driving GraphQL 10. Building a Logo Interpreter 11. Adding Animation 12. Working with WebSockets 13. Writing Your First Acceptance Test 14. Adding Features Guided by Acceptance Tests 15. Understanding TDD in the Wider Testing Landscape

Rendering lists and detail views

The Git tag for this section is appointments-day-view.

So far, we’ve seen a great deal of test-driven development, but not much of React. In this section, we’ll take what we’ve learned about TDD and apply it to learning more React.

Our app at the moment just displays a single thing—a customer’s name. Now, we'll extend it so that we have a view of all appointments that are happening today.

Let's do a little more up-front design. We've got an Appointment component that takes an appointment and displays it. We can build an AppointmentsDayView component around it that takes an array of appointment objects and displays them as a list. It also displays a single Appointment component at any one time, whichever appointment is currently selected. The user can click on an Appointment and it will open up that appointment for viewing:

Rendering the list of appointments

We'll add our new component into the same file we've been using already because there's not much code in there so far.

We don't always need a new file for each component, particularly when the components are short functional components, such as our Appointment component (a one-line function). It can help to group related components or small sub-trees of components in one place.

In test/Appointment.test.js, create a new describe block under the first one, with a single test, as follows. This test checks that we render a div with a particular ID. That's important in this case because we load a CSS file that looks for this element. The expectations in this test use the DOM method, querySelector. This searches the DOM tree for a single element with the tag provided:

describe('AppointmentsDayView', () => {
let container;

beforeEach(() => {
container = document.createElement('div');
});

const render = component =>
ReactDOM.render(component, container);

it('renders a div with the right id', () => {
render(<AppointmentsDayView appointments={[]} />);
expect(container.querySelector('div#appointmentsDayView')).not.toBeNull();
});
});
It isn't always necessary to wrap your component in a div with an ID or a class. I tend to do it when I have CSS that I want to attach to the entire group of HTML elements that will be rendered by the component, which, as you'll see later, is the case for AppointmentsDayView.

This test uses the exact same render function from the first describe block, as well as the same let container declaration and beforeEach block. In other words, we've introduced duplicated code. By duplicating code from our first test suite, we're making a mess straight after cleaning up our code! Well, we're allowed to do it when we're in the first stage of the TDD cycle. Once we've got the test passing, we can think about the right structure for the code.

Run npm test and let's look at the output:

FAIL test/Appointment.test.js
Appointment
✓ renders the customer first name (18ms)
✓ renders another customer first name (2ms)
AppointmentsDayView
✕ renders a div with the right id (7ms)

● AppointmentsDayView › renders a div with the right id

ReferenceError: AppointmentsDayView is not defined

Let's work on getting this test to pass!

  1. To fix this, change the last import in your test file to read as follows:
import {
Appointment,
AppointmentsDayView
} from '../src/Appointment';
  1. In src/Appointment.js, add this functional component below Appointment:
export const AppointmentsDayView = () => null;
When we first defined our Appointment component earlier, we didn't return null. In fact, we didn't return anything. React then gave us a test error that we needed to fix before we got to a helpful test failure. So, returning null allows us to skip past the error from React and will bring us directly to a test failure. I'll generally begin all my components in this way—with a null value.
  1. Run your tests again:
  ● AppointmentsDayView › renders a div with the right id

expect(received).not.toBeNull()

Received: null

47 | it('renders a div with the right id', () => {
48 | render(<AppointmentsDayView appointments={[]} />);
> 49 | expect(container.querySelector('div#appointmentsDayView')).not.toBeNull();
| ^
50 | });
  1. Finally, a test failure! Let's get that div in place:
export const AppointmentsDayView = () =>
<div id="appointmentsDayView"></div>;
  1. Your test should now be passing. Let's move on to the next test. Add the following text, just below the last test in test/Appointment.test.js, still inside the AppointmentsDayView describe block:
it('renders multiple appointments in an ol element', () => {
const today = new Date();
const appointments = [
{ startsAt: today.setHours(12, 0) },
{ startsAt: today.setHours(13, 0) }
];
render(<AppointmentsDayView appointments={appointments} />);
expect(container.querySelector('ol')).not.toBeNull();
expect(
container.querySelector('ol').children
).toHaveLength(2);
});
  1. Run your tests:
expect(received).not.toBeNull()

Received: null

57 | ];
58 | render(<AppointmentsDayView appointments={appointments} />);
> 59 | expect(container.querySelector('ol')).not.toBeNull();
| ^
60 | expect(container.querySelector('ol').children).toHaveLength(2);
61 | });
62 | });

at Object.toBeNull (test/Appointment.test.js:48:47)
In the test, the today constant is defined to be new Date(). Each of the two records then uses this as a kind of "base" date to work its own time off. Whenever we're dealing with dates, it's important that we base all events on the same moment in time, rather than asking the system for the current time more than once. Doing that is a subtle bug waiting to happen.
  1. Let's add the ol element. Remember not to jump ahead; at this point, we just need ol to be there, not including the two items:
export const AppointmentsDayView = () => (
<div id="appointmentsDayView">
<ol />
</div>
);
  1. Run npm test again. The test output is now as follows:
Expected length: 2
Received length: 0
Received object: []

47 | render(<Appointments appointments={appointments} />);
48 | expect(container.querySelector('ol')).not.toBeNull();
> 49 | expect(container.querySelector('ol').children).toHaveLength(2);
| ^
50 | });
51 | });
52 |
  1. Since we've got multiple expectations in this test, the stack trace is essential in highlighting which expectation failed. This time, it's the second expectation: we've got zero children in the ol element but we want two. To fix this, as always, we'll do the simplest thing that will possibly work, as follows:
export const AppointmentsDayView = ({ appointments }) => (
<div id="appointmentsDayView">
<ol>
{appointments.map(() => (
<div />
))}
</ol>
</div>
);
The map function will provide a single argument to the function passed to it. Since we don't use the argument (yet), we don't need to assign it in the function signature—we can just pretend that our function has no arguments instead, hence the empty brackets. Don't worry, we'll need the argument for a subsequent test and we'll add it in then.
  1. If we're being strict, this isn't quite right: ol elements should not have div elements for children. But, that's all we should need to pass the test. We can use the next test to make sure the children are li elements. Let's see what Jest says; run npm test again:
PASS test/Appointment.test.js
Appointment
✓ renders the customer first name (19ms)
✓ renders another customer first name (2ms)
AppointmentsDayView
✓ renders a div with the right id (7ms)
✓ renders multiple appointments in an ol element (16ms)

console.error node_modules/react/cjs/react.development.js:217
Warning: Each child in an array or iterator should have a unique "key" prop.
  1. Our test passed, but we got a warning from React. It's telling us to set a key value on each li element. We can use startsAt as a key:
<ol>
{appointments.map(appointment => (
<div key={appointment.startsAt} />
))}
</ol>
Unfortunately there's no easy way for us test key values in React. To do it, we'd need to rely on internal React properties, which would make our tests at risk of breaking if the React team were to ever change those properties.

The best we can do is set a key to get rid of this warning message. Any value will do: unfortunately we can't use TDD to specify how keys are formed.

In this case, I'd quite like a test that uses the startsAt timestamp for each li key. Let's just imagine that we have that test in place.

Selecting data to view

Let's add in some dynamic behavior to our page. We'll make each of the list items a link that the user can click on to view that appointment.

Thinking through our design a little, there are a few pieces we'll need:

  • A button element within our li
  • An onClick handler that is attached to that button element
  • A component state to record which appointment is currently being viewed

When we test React actions, we do it by observing the consequences of those actions. In this case, we can click on a button and then check that its corresponding appointment is now rendered on screen.

Initial selection of data

Let's start by asserting that each li element has a button element:

  1. First up, let's display a message to the user if there are no appointments scheduled for today. In the AppointmentsDayView describe block, add this test:
it('initially shows a message saying there are no appointments today', () => {
render(<AppointmentsDayView appointments={[]} />);
expect(container.textContent).toMatch(
'There are no appointments scheduled for today.'
);
});
  1. Make that pass by adding in a message at the bottom of rendered output. We don't need a check for an empty appointments array just yet; we'll need another test to triangulate to that:
return (
<div id="appointmentsDayView">
...
<p>There are no appointments scheduled for today.</p>
</div>
);
  1. If there are appointments scheduled, then we start off by showing the first one of the day. We can check for a rendered customer firstName to determine whether the right customer is shown:
it('selects the first appointment by default', () => {
render(<AppointmentsDayView appointments={appointments} />);
expect(container.textContent).toMatch('Ashley');
});
  1. Since we're looking for the customer name, we'll need to make sure that's available in the appointments array. Update it now to include the customer firstName:
  const appointments = [
{
startsAt: today.setHours(12, 0),
customer: { firstName: 'Ashley' }
},
{
startsAt: today.setHours(13, 0),
customer: { firstName: 'Jordan' }
}
];
  1. Let's make that pass by using our Appointment component. Modify the last line of the div component to read as follows:
<div id="appointmentsDayView">
// ... existing code here ...
{appointments.length === 0 ? (
<p>There are no appointments scheduled for today.</p>
) : (
<Appointment {...appointments[0]} />
)}
</div>

Now, we're ready to let the user make a selection.

Adding events to a functional component

We're about to add state to our component. The component will show a button for each appointment. When the button is clicked, the component stores the array index of the appointment that it refers to. To do that, we'll use the useState hook.

Hooks are a feature of React that manage various non-rendering related operations. The useState hook stores data across multiple renders of your function. The call to useState returns you both the current value in storage and a setter function that allows it to be set.

If you're new to hooks, check out the Further learning section at the end of this chapter. Alternatively, you could just follow along and see how much you can pick up just by reading the tests!

Let's start by asserting that each li element has a button element:

  1. Add the following test, just below the last one you added. The second expectation is a little peculiar in that it is checking the type of the button to be button. If you haven't seen this before, it's idiomatic when using button elements to define its role by setting the type attribute, as I'm doing here:
it('has a button element in each li', () => {
render(<AppointmentsDayView appointments={appointments} />);
expect(
container.querySelectorAll('li > button')
).toHaveLength(2);
expect(
container.querySelectorAll('li > button')[0].type
).toEqual('button');
});
We don't need to be pedantic about checking the content or placement of the button element within its parent. For example, this test would pass if we put an empty button child at the end of li. But, thankfully, doing the right thing is just as simple as doing the wrong thing, so we can opt to do the right thing instead. All we need to do to make this pass is wrap the existing content in the new tag.
  1. Make this test pass by modifying the AppointmentsDayView return value, as shown:
<ol>
{appointments.map(appointment => (
<li key={appointment.startsAt}>
<button type="button">
{appointmentTimeOfDay(appointment.startsAt)}
</button>
</li>))}
</ol>;
  1. We can now test what happens when the button is clicked. Back in test/Appointment.test.js, add the following as the next test. This uses the ReactTestUtils.Simulate.click function to perform the click action:
it('renders another appointment when selected', () => {
render(<AppointmentsDayView appointments={appointments} />);
const button = container.querySelectorAll('button')[1];
ReactTestUtils.Simulate.click(button);
expect(container.textContent).toMatch('Jordan');
});
React components respond to what it calls synthetic events. React uses these to mask browser discrepancies in the DOM event model. That means we can't raise standard events that we'd fire through JSDOM. Instead, we use the ReactTestUtils.Simulate object to raise events.
  1. Include the following import at the top of test/Appointment.test.js:
import ReactTestUtils from 'react-dom/test-utils';
  1. Go ahead and run the test:
  ● AppointmentsDayView › renders appointment when selected

expect(received).toMatch(expected)

Expected value to match:
"Jordan"
Received:
"12:0013:00Ashley"

We're getting all of the list content dumped out too, because we've used container.textContent in our expectation rather than something more specific.

At this stage, I'm not too bothered about where the customer name appears on screen. Testing container.textContent is like saying I want this text to appear somewhere, but I don't care where. Later on, we'll see techniques for expecting text in specific places.

There's a lot we now need to get in place in order to make the test pass: we need to introduce state and we need to add the handler. But, first, we'll need to modify our definition to use a block with a return statement:

  1. Set the last test to skip, using it.skip.
We never refactor on red. It's against the rules! But if you're on red, you can cheat a little by rewinding to green by skipping the test that you've just written.

It may seem a little pedantic to do that for the very tiny change we're about to make, but it's good practice.
  1. Wrap the constant definition in curly braces, and then return the existing value. Once you've made this change, run your tests and check you're all green:
export const AppointmentsDayView = ({ appointments }) => {
return (
<div id="appointmentsDayView">
<ol>
{appointments.map(appointment => (
<li key={appointment.startsAt}>
<button type="button">
{appointmentTimeOfDay(appointment)}
</button>
</li>))}
</ol>
<Appointment customer={appointments[0].customer} />
</div>
);
};
  1. Unskip the latest test by changing it.skip to it, and let's get to work on making it pass.
  2. Update the import at the top of the file to pull in the useState function:
import React, { useState } from 'react';
  1. Add the following line above the return statement:
const [selectedAppointment, setSelectedAppointment] = useState(
0
);
  1. We can now use this selectedAppointment rather than hard-coding an index selecting the right appointment. Change the return value to use this new state value when selecting an appointment:
<div id="appointmentsDayView">
...
<Appointment {...appointments[selectedAppointment]} />
</div>
  1. Then, change the map call to include an index in its arguments. Let's just name that i:
{appointments.map((appointment, i) => (
<li key={appointment.startsAt}>
<button type="button">
{appointmentTimeOfDay(appointment.startsAt)}
</button>
</li>
))}
  1. Now call setSelectedAppointment from within the onClick handler on the button element:
<button
type="button"
onClick={() => setSelectedAppointment(i)}>
  1. Run your tests, and you should find they're all green:
PASS test/Appointment.test.js
Appointment
✓ renders the customer first name (18ms)
✓ renders another customer first name (2ms)
AppointmentsDayView
✓ renders a div with the right id (7ms)
✓ renders multiple appointments in an ol element (16ms)
✓ renders each appointment in an li (4ms)
✓ initially shows a message saying there are no appointments today (6ms)
✓ selects the first element by default (2ms)
✓ has a button element in each li (2ms)
✓ renders another appointment when selected (3ms)

Our component is now complete and ready to be used in the rest of our application. That is, once we've built the rest of the application!

Manually testing our changes

The Git tag for this section is entrypoint.

The words manual testing should strike fear into the heart of every TDDer. Manual testing takes up so much time. I usually avoid it if I can. That being said, even if we wanted to manually test, we couldn't as we can't yet run our app. To do that, we'll need to add an entrypoint.

Putting it all together with Webpack

Jest includes Babel, which transpiles all our code when it's run in the test environment. But what about when we're serving our code via our website? Jest won't be able to help us there.

That's where Webpack comes in, and we can introduce it now to help us, do a quick manual test:

  1. Install Webpack using the following command:
npm install --save-dev webpack webpack-cli babel-loader
  1. Add the following to the scripts section of your package.json:
"build": "webpack",
  1. You'll also need to set some configuration for Webpack. Create the webpack.config.js file in your project root directory with the following content:
const path = require("path");
const webpack = require("webpack");

module.exports = {
mode: "development",
module: {
rules: [{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader'}]}
};

This configuration works for Webpack in development mode. Consult the Webpack documentation for information on setting up production builds.

  1. In your source directory, run the following commands:
mkdir dist
touch dist/index.html
  1. Add the following content to the file you just created:
<!DOCTYPE html>
<html>
<head>
<title>Appointments</title>
</head>
<body>
<div id="root"></div>
<script src="main.js"></script>
</body>
</html>
  1. You're now ready to run the build:
npm run build

You should see a bunch of output like this:

  Asset Size Chunks Chunk Names
main.js 764 KiB main [emitted] main
Entrypoint main = main.js
[./src/Appointment.js] 4.67 KiB {main} [built]
[./src/index.js] 544 bytes {main} [built]
[./src/sampleData.js] 726 bytes {main} [built]
+ 11 hidden modules
  1. Open index.html in your browser and behold your creation:
The following screenshot shows the application once the Exercises are completed, and with added CSS and extended sample data. To include the CSS, you'll need to pull dist/index.html and dist/styles.css from the chapter-2 tag. The sample data can be found in src/sampleData.js, within the same tag. If you're choosing not to complete the Exercises, you can skip to that tag now.

As you can see, we've only got a little part of the way to fully building our application. The first few tests of any application are always the hardest and take the longest to write. We are now over that hurdle, so we'll move quicker from here onward.

You have been reading a chapter from
Mastering React Test-Driven Development
Published in: May 2019
Publisher: Packt
ISBN-13: 9781789133417
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at £13.99/month. Cancel anytime
Visually different images