Mastering React Test-Driven Development

5 (1 reviews total)
By Daniel Irvine
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. First Steps with Test-Driven Development

About this book

Many programmers are aware of TDD but struggle to apply it beyond basic examples. This book teaches how to build complex, real-world applications using Test-Driven Development (TDD). It takes a first principles approach to the TDD process using plain Jest and includes test-driving the integration of libraries including React Router, Redux, and Relay (GraphQL).

Readers will practice systematic refactoring while building out their own test framework, gaining a deep understanding of TDD tools and techniques. They will learn how to test-drive features such as client- and server-side form validation, data filtering and searching, navigation and user workflow, undo/redo, animation, LocalStorage access, WebSocket communication, and querying GraphQL endpoints.

The book covers refactoring codebases to use the React Router and Redux libraries. via TDD. Redux is explored in depth, with reducers, middleware, sagas, and connected React components. The book also covers acceptance testing using Cucumber and Puppeteer.

The book is fully up to date with React 16.9 and has in-depth coverage of hooks and the ‘act’ test helper.

Publication date:
May 2019
Publisher
Packt
Pages
496
ISBN
9781789133417

 

Chapter 1. First Steps with Test-Driven Development

This book follows a simple format: it's a walk-through of building React applications using a test-driven approach. We'll touch on many different parts of the React experience, including building forms, composing interfaces, and animating elements. We'll also integrate React Router, Redux, and GraphQL, all guided by tests. The focus isn't on how these features of React work, but rather on how to test them and make sure you're using them with confidence.

ModernJavaScript programmers rely heavily on packages that other people have developed. This allows us to concentrate on innovating, not reinventing, the wheel. The downside, however, is that we don't always have a full understanding of the technologies we’re dealing with. We simply don't need to learn them.

Among other things, Test-Driven Development(TDD) is an effective technique for learning new frameworks and libraries. That makes it very well suited for a book on React and its ecosystem. This book will allow you to explore React in a way that you may not have experienced before.

If you're new to TDD, some of the steps outlined may leave you scratching your head. You may find yourself wondering why we're going to such Herculean efforts to build an application. There is tremendous value to be gained in specifying our software in this way. By being crystal clear about our requirements, we gain the ability to adapt our code without fear of change. We gain automated regression testing by default. Our tests comment our code, and those comments are verifiable when we run them. We gain a method of communicating our decision-making process with our colleagues. And you'll soon start to recognize the higher level of trust and confidence you have in the code you're working on. If you're anything like me, you'll get hooked on that feeling and find it hard to work without it.

Sections 1 and 2 of this book involve building an appointments system for a hair salon—nothing too revolutionary, but as sample applications go, it offers plenty of scope. We'll get started with that in this chapter. Sections 3 and 4 use an entirely different application: a Logo interpreter. Building that offers a fun way to explore more of the React landscape.

This chapter, and in fact this whole book, takes a first principles approach to React. We start with minuscule steps to slowly uncover the TDD story. We'll prefer rolling our own code to using libraries and packages. We will start from an empty directory and begin building out our application, test by test. Along the way, we’ll discover a lot of the fundamental ideas behind test-driven development and React.

The following topics will be covered in this chapter:

  • Creating a new React project from scratch
  • Displaying data with your first test
  • Refactoring your work
  • Writing great tests
  • Rendering lists and detail views
 

Technical requirements


Later in this chapter, you'll be required to install Node Package Manager (npm) together with a whole host of packages. You'll want to ensure you have a machine capable of running the Node.js environment.

You'll also need access to the command line.

In addition, you should choose a good editor or Integrated Development Environment (IDE) to work with your code.

 

Creating a new React project from scratch


There's a standard template for creating React apps: the create-react-app application template. This includes some standard dependencies and boilerplate code that all React applications need. However, it also contains some extra items such as favicon.ico, a sample logo, and CSS files. While these are undoubtedly useful, having them here at the very start of a project is at odds with one of the test-driven developer's core principles: You Ain't Gonna Need It (YAGNI).

This principle says that you should hold off adding anything to your project until you're really sure that it's necessary. Perhaps that's when your team adds a user story for it into the iteration, or maybe it's when a customer asks for it. Until then, YAGNI.

It's a theme that runs throughout this book and we'll start right now by choosing to avoid create-react-app. You can always start every JavaScript project from scratch, and there's a certain joy to be found in going over the basics each time.

Installing NPM

We’ll be making extensive use of the npm command-line tool and the Node.js execution environment. Each time you run your tests, which will be very frequently, you'll be required to run an npm command.

Toward the end of the chapter, we'll also use npm to package our application.

You can find out if you already have it installed on your machine by opening a Terminal window (or Command Prompt if you’re on Windows) and typing the following:

npm -v

If the command isn’t found, head on over to the Node.js website for details on how to install. The URL is included at the end of this chapter.

The npm program knows how to update itself, so if it's installed, I recommend you ensure you’re on the latest version. You can do this on the command line by typing this:

npm install [email protected] -g

I'm using version 6.9.0 to write this book. If you have any issues with the code samples contained here, differing NPM versions could be one of the causes, so please bear that in mind as you continue.

Note

Yet another resource negotiator (YARN) is an alternative to NPM, and I won’t hold it against you if you choose to use it. There are only a handful of npm commands in this book—I assume that if you’re sticking with YARN, then you’ll already know how to convert npm commands to yarn commands.

Creating a new Jest project

Note

The Git tag for this section is starting-point. It doesn't contain any code; just a README.md file. If you want to follow along using the book's Git repository then you should ensure you've branched from this tag. Detailed instructions from doing that are in the Getting started before Chapter 1 section of the Preface.

Now that NPM is installed, we can create our project:

  1. If you're following along with the book's Git repository, open a Terminal window and navigate to the repository directory that you cloned in the Getting started before Chapter 1 section of the Preface. Otherwise, simply navigate to your local projects directory.
  2. Create a new directory using mkdir appointments and then change to it using cd appointments.
  3. Enter the npm initcommand, which begins the process of initializing a new NPM project and generating a package.json file for you.
  4. The first questions ask you to provide a package name, version, description, and an entrypoint. Since we're building an appointments system, you can call it appointments. Accept the default version (by just hittingEnter), and enter a description ofAppointments system. You can accept the default entrypoint too.
  5. Next, you'll be asked for a testcommand, for which you should type injest. This will enable you to run tests by using the npm test shortcut command.

Note

Don't worry if you miss this; you can set it afterward by adding "test": "jest" to the scripts section of the generated package.json.

  1. You'll be asked to specify a repository, which you could just set as example.comfor now. If you don’t fill these fields in,npmwill print warnings every time you run a command.
  1. You can accept the defaults for everything else.

Note

You may wonder why we filled out the repository field. TDD lovesfast feedback cycles. Prioritize cleaning your screen and command outputs of as much noise as possible. Any time you see something that is destroying clarity, either fix it right then and there, or put it as an action at the top of your to-do list. In this particular case, you could also add "private": true to your package.json, instead of setting the repository field.

  1. Hit Enter on the remaining questions to finish the initialization process.
  2. Install Jest using npm install --save-dev jest.

You will see the bottom line of your Terminal fill up with a fast-changing stream of package information as NPM installs dependent packages (a paltry 553 packages at the time of writing). You may see some warnings depending on the platform you are installing on, but these can be ignored. Once complete, you should see this:

npm notice created a lockfile as package-lock.json. You should commit this file.

+ [email protected]
+ added 553 packages from 373 contributors and audited 849842 packages in 16.304s
+ found 0 vulnerabilities

Commit early and often

The second sentence of that command output (You should commit this file) is a good cue for us to commit for the first time.

TDD provides natural breakpoints for you to commit code. If you’re starting out with TDD, I’d recommend committing to source control after every single test. That might seem like overkill for your projects at work, but as you're learning, it can be a very effective tool.

If you've ever watched The Weakest Link, you'll know that contestants can choose to bank their winnings at any time, which decreases their risk of losing money but reduces their earning potential. With git, you can use git addto effectively bank your code. This saves a snapshot of your code but does not commit it. If you make a mess in the next test, you can revert to the last banked state. I tend to do this after every test. And, unlike in The Weakest Link, there's no downside to banking!

Committing early and often simplifies commit messages. If you have just one test in a commit, then you can use the test description as your commit message. No thinking is required.

If you're using git, use the following commands to commit what you’ve done so far:

git init
echo "node_modules" > .gitignore
git add .
git commit -m "Blank project with Jest dependency"

Bringing in React and Babel

Let's install React. That's actually two packages:

npm install --save react react-dom

React makes heavy use of JavaScriptXML (JSX), which we need Babel to transpile for us. Babel also transpiles our modern ES6 and ES7 constructs for us.

Note

The following information is accurate for Babel 7. If you're using a later version, you may need to adjust the installation instructions accordingly.

Thankfully, Jest already includes Babel, so we just need to install presets and plugins:

npm install --save-dev @babel/preset-env @babel/preset-react
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime

A Babel preset is a set of plugins. Each plugin enables a specific feature of the ECMAScript standards, or a preprocessor such as JSX.

The env preset brings in essentially everything possible. It should really be configured with target execution environments. See the Further reading section at the end of this chapter for more information.

We need to enable the packages we've just installed. Create a new file, .babelrc, and add the following:

{
  "presets": ["@babel/env", "@babel/react"],
  "plugins": ["@babel/transform-runtime"]
}

With that, you're all set to write some tests. You may wish to check in at this point.

 

Displaying data with your first test


Note

The Git tag for this section is appointment-first-name.

In this section, we'll discover the TDD cycle for the first time.

We'll start our application by building out an appointment view. We won't get very far; the tests we'll create in this chapter will simply display the customer who made the appointment. As we do so, we'll discuss the TDD process in detail.

We'll build a React functional component called Appointment. It is used for displaying the details of a single appointment in our system. The component will be passed in a data structure that represents Appointment, which we can imagine looks a little something like this:

{
  customer: { firstName: 'Ashley', lastName: 'Jones', phoneNumber: '(123) 555-0123' },
  stylist: 'Jay Speares',
  startsAt: '2019-02-02 09:30',
  service: 'Cut',
  notes: ''
}

We won't manage to get all of that information displayed by the time we complete the chapter; in fact, we'll only display the customer's firstName, and we'll make use of the startsAt timestamp to order a list of today's appointments.

But before we get on to that, let's explore Jest a little.

Writing a failing test

What exactly is a test? We'll discover that by writing one. In your project directory, type the following commands:

mkdir test
touch test/Appointment.test.js

Open the test/Appointment.test.jsfile in your favorite editor or IDE and enter the following:

describe('Appointment', () => {
});

The describe function defines a test suite, which is simply a set of tests with a given name. The first argument is the name (or description) of the unit you are testing. It could be a React component, a function, or a module. The second argument is a function inside of which you define your tests.

Note

All of the Jest functions are already required and available in the global namespace when you run the npm test command. You don't need to import anything.

For React components, it's good practice to give your describe blocks the same name as the component itself.

You should run this code right now in the Jest test runner. It will give us valuable information about what to do next. You might think that running tests now is pointless, since we haven't even written a test yet, but with TDD, it's normal to run your test runner at every opportunity.

On the command line, run the npm testcommand:

> [email protected] test /home/daniel/work/react-tdd/ch1
> jest

FAIL test/Appointment.test.js
● Test suite failed to run

Your test suite must contain at least one test.

  at node_modules/jest/node_modules/jest-cli/build/TestScheduler.js:225:24

Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 0.917s
Ran all test suites.
npm ERR! Test failed. See above for more details.

You can see Jest helpfully tells us Your test suite must contain at least one test. Test-driven developers rely heavily on listening to the test runner and what it tells us. It usually tells them exactly what to do next. In this case, it's telling us to create a test. So, let's do that.

Note

Where should you place your tests?If you do try out thecreate-react-apptemplate, you’ll notice that it contains a single unit test file,App.test.js, which exists in the same directory as the source file,App.js. I don't recommend mixing production code with test code. For a start, it isn’t the conventional unit-testing approach, which uses two separate directories for production code and test code. More importantly, however, it’s likely that you won’t have a one-to-one mapping between production and test files.

Writing your first expectation

Change your describe call to this:

describe('Appointment', () => {
  it('renders the customer first name', () => {
  });
});

The it function defines a single test. The first argument is the description of the test and always starts with a present-tense verb, so that it reads in plain English. The it in the function name refers to the noun you used to name your test suite (in this case, Appointment). In fact, if you run tests now, with npm test, remember, it should make sense:

PASS test/Appointment.test.js
  Appointment
    ✓ renders the customer first name (1ms)

You can read the describe and it descriptions together as one sentence: Appointment renders the customer first name. You should aim for all of your tests to be readable in this way.

As we add more tests, Jest will show us a little checklist of passing tests.

Note

You may have used the test function for Jest, which is equivalent to it. Since we’re doing behavior driven development style of TDD, you should stick with it.

Empty tests, such as the one we just wrote, always pass. Let's change that now. Let's add an expectation to our test. Change test to read as follows:

it('renders the customer first name', () => {
  expect(document.body.textContent).toMatch('Ashley');
});

This expect call is an example of a fluent API. Like the test description, it reads like plain English. You can read it like this: I expect document.body.textContenttoMatch the stringAshley.

Although it might look complicated, it's quite a simple idea: each expectation has an expected value that is compared against a received value. In this example, the expected value is Ashley and the received value is whatever is stored in document.body.textContent.

The toMatch function is called a matcher and there are a whole lot of different matchers that work in different ways. In this case, the expectation passes if document.body.textContent has the word Ashley anywhere within it.

Each individual test can have as many expectations in it as you like, and we'll see examples of multiple expectations in a test later in this chapter.

Before we run this test, spend a minute thinking about the code. You might have guessed that the test will fail. The question is, how will it fail?

Let's run test now, with npm test, and find out:

FAIL test/Appointment.test.js
  Appointment
    ✕ renders the customer first name (10ms)

  ● Appointment › renders the customer first name

    expect(received).toMatch(expected)

    Expected value to match:
      "Ashley"
    Received:
      ""

      1 | describe('Appointment', () => {
      2 |   it('renders the customer first name', () => {
    > 3 |     expect(document.body.textContent).toMatch('Ashley');
        |                                       ^
      4 |   });
      5 | });
      6 |

      at Object.toMatch (test/Appointment.test.js:3:39)

There are four parts to the test output that are relevant to us:

  • The name of the failing test
  • The expected answer
  • The actual answer
  • The location in the source where the error occurred

All of these help us to pinpoint where our tests failed: document.body.textContent is empty. This isn't surprising really, since we've not done anything to set the body text.

But, hold on a second. Where did document.body come from? No one defined that yet. Shouldn’t we expect the test to fail with an error saying that the document is undefined?

Jest magically includes a DOM implementation for us, which is why we have access to document and document.body. It uses jsdom, a headless implementation of the DOM. We can do test browser interactions on the command line, which is much simpler than involving a browser in our work.

In Jest lingo, this is called the Jest environment and it defaults to jsdom. If you want to verify that this is happening, add the following config to your package.json file:

"jest": {
  "testEnvironment": "node"
}

Re-run tests and observe the different output to convince yourself that JSDOM is no longer present.

Be sure to remove this extra configuration before you continue, as we’ll be relying on the JSDOM environment from now on.

Rendering React from a test

In order to make this test pass, we'll have to write some code above the expectation that will call into our production code.

Since we're testing what happens when a React component is rendered, we'll need to call the ReactDOM.render function. This function takes a component (which in our case will be called Appointment), performs the React render magic, and replaces an existing DOM node with the newly rendered node tree. The DOM node it replaces is known as the React container.

Here's the method signature:

ReactDOM.render(component, container)

In order to call this in our test, we'll need to define both component and container. Let's piece the test together before we write it out in full. It will have this shape:

it('renders the customer first name', () => {
  const component = ???
  const container = ???
  ReactDOM.render(component, container);
  expect(document.body.textContent).toMatch('Ashley');
});

Since we're rendering Appointment, we know what we need to put for component. It's a JSX fragment that takes our customer as a prop:

 const customer = { firstName: 'Ashley' };
 const component = <Appointment customer={customer} />;

Note

Back when we were considering our design, we came up with a whole object format for our appointments. You might think the definition of a customer here is very sparse, as it only contains a first name. But we don't need anything else for a test about customer names.

What about container? We can use the DOM to create a container element:

const container = document.createElement('div');
document.body.appendChild(container);

Now let's take a look at that test in full. Change your test in test/Appointments.test.js to match the following:

it('renders the customer first name', () => {
  const customer = { firstName: 'Ashley' };
  const component = <Appointment customer={customer} />;
  const container = document.createElement('div');
  document.body.appendChild(container);

  ReactDOM.render(component, container);

  expect(document.body.textContent).toMatch('Ashley');
});

As we're using both ReactDOM and JSX, we'll need to include the two standard React import at the top of our test file for this to work, as follows:

import React from 'react';
import ReactDOM from 'react-dom';

Go ahead and run the test. Within the output, you'll see the following:

ReferenceError: Appointment is not defined

This is subtly different from the test failure we saw previously. This is a run-time exception, not an expectation failure. Thankfully, though, the exception is telling us exactly what we need to do, just as a test expectation would. We need to define Appointment.

Make it pass

We're now ready to make failing test pass:

  1. Add import to test/Appointment.test.js, below the two React imports:
import { Appointment } from '../src/Appointment';
  1. Run tests with npm test. You'll get a different error this time:
Cannot find module '../src/Appointment' from 'Appointment.test.js'

Note

Although Appointment was defined as an export, it wasn't defined as a default export. That means we have to import it using the curly brace from of import (import { ... }). I tend to avoid using default exports; doing so keeps the name of my component and its usage in sync: if I change the name of a component, then every place where it's imported will break unless I change those too. This isn't the case with default exports. Once your names are out of sync, it can be hard to track where components are used.

  1. Let's create that module. Type the following at your command line:
mkdir src
touch src/Appointment.js
  1. In your editor, add the following content to src/Appointment.js:
export const Appointment = () => {};

Why have I created a shell of an Appointment without actually creating an implementation? This might seem pointless, but another core principle of the test-driven developer is always do the simplest thing to pass the test. We could rephrase this as always do the simplest thing to fix the error you're working on.

Remember when I mentioned that we listen carefully to what the test runner tells us? In this case, the test runner said Cannot find module Appointment, so what was needed was to create that module:

  1. Run npm test. You'll get a lot of React output as a large stack trace. If you scroll up to the top, you'll see this:
Error: Uncaught [Invariant Violation: Appointment(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.]
  1. To fix that, we need to do what it's telling us: we need to return something "from render". So, let's return something. Change the file to read as follows:
import React from 'react';

export const Appointment = () => <div></div>;
  1. Now, if you run the test, you should get a test failure:
FAIL test/Appointment.test.js
  Appointment
    ✕ renders the customer first name (23ms)

  ● Appointment › renders the customer first name

    expect(received).toMatch(expected)

    Expected value to match:
      "Ashley"
    Received:
      ""
  1. To fix the test, change the Appointment definition to look like this:
export const Appointment = () => (
  <div>Ashley</div>
);

But, wait a second. This test isn't using ourappointmentvariable that we defined in our test. We just hard-coded a value of Ashley in there!

Remember our principle: always implement the simplest thing that will possibly work. That includes hard-coding, when it's possible. In order to get to the real implementation, we need to add more tests. This process is called triangulation. The more specific our tests get, the more general our production code needs to get.

Note

This is one reason why pair programming using TDD can be so fun. Pairs can play ping pong. Sometimes, your pair will write a test that you can solve trivially, perhaps by hard-coding, and then you force them to do the hard work of both tests by triangulating. They need to remove the hard-coding and add the generalization.

Let's triangulate:

  1. Make a copy of your first test, pasting it just under the first test, and change the test description and the name of Ashley to Jordan, as follows:
it('renders another customer first name', () => {
  const customer = { firstName: 'Jordan' };
  const component = <Appointment customer={customer} />;
  const container = document.createElement('div');
  document.body.appendChild(container);

  ReactDOM.render(component, container);

  expect(document.body.textContent).toMatch('Jordan');
});
  1. Run tests with npm test.We expect this test to fail, and it does. Take a careful look at thisoutput:
FAIL test/Appointment.test.js
  Appointment
  ✓ renders the customer name (19ms)
  ✕ renders another customer name (20ms)

● Appointment › renders another customer name

  expect(received).toMatch(expected)

  Expected value to match:
    "Jordan"
  Received:
    "AshleyAshley"

Yes, it did fail—but with the text AshleyAshley!

This kind of repeated text is an indicator that our tests are not running independently of one another. There is some shared state that isn't being cleared. We need to change course and uncover what's going on.

Note

Unit tests should be independent of one another. The simplest way to achieve this is to not have any shared state between tests. Each test should only use variables that it has created itself.

Backtracking on ourselves

There's only one piece of shared state that our tests use and that's document. It must not be getting cleared each time the tests are run, and so we see the output of each test inside the document.

Even if we fixed our production code to remove the hard-coding, it still wouldn't pass; instead, we'd see the text AshleyJordan.

One solution is to clear the document DOM tree before each test run. But there's a simpler solution: we can rework our tests to not append our container element to the DOM at all, and instead work directly with the container element. In other words, we can change our expectation to check not document.body.textContent but container.textContent.

Note

There may come a time that we actually need to attach our nodes to the DOM, and at that point, we'll need to fix this problem properly. But for now, you ain't gonna need it. So, let's solve this by avoiding the DOM tree altogether. It's the simplest way forward.

Unfortunately, there's a problem. We're in the middle of a red test. We should never refactor, rework, or otherwise change course while we're red.

What we'll have to do is ignore, or pend, this test we're working on. We do that by changing the word it to it.skip. Do that now for the second test:

it.skip('renders another customer first name', () => {

Run tests. You'll see Jest ignores the second test, and the first one still passes:

PASS test/Appointment.test.js
  Appointment
    ✓ renders the customer first name (19ms)
    â—‹ skipped 1 test

Test Suites: 1 passed, 1 total
Tests: 1 skipped, 1 passed, 2 total

For this refactor, we need to make two changes:

  • Change the expectation to match on container.textContent.
  • Remove the line that calls appendChild on the document body.

We can also take this opportunity to inline the component variable. Change the test to read as follows:

it('renders the customer first name', () => {
  const customer = { firstName: 'Ashley' };
  const container = document.createElement('div');
  ReactDOM.render(<Appointment customer={customer} />, container);
  expect(container.textContent).toMatch('Ashley');
});

Run your tests: the result should be the same as earlier, with one passing test and one skipped.

It's time to bring that second test back in, by removing the .skip from the function name, and this time, let's update the test code to make the same changes we made in the first, as follows:

it('renders another customer first name', () => {
  const customer = { firstName: 'Jordan' };
  const container = document.createElement('div');
  ReactDOM.render(<Appointment customer={customer} />, container);
  expect(container.textContent).toMatch('Jordan');
});

Running tests now should give us the error that we were originally expecting:

FAIL test/Appointment.test.js
  Appointment
    ✓ renders the customer first name (18ms)
    ✕ renders another customer first name (8ms)

  ● Appointment › renders another customer first name

    expect(received).toMatch(expected)

    Expected value to match:
      "Jordan"
    Received:
      "Ashley"

To fix this, we need to introduce the variable and use it within our JSX, which supports embedding JavaScript expressions within elements. We can also use destructuring assignment to avoid creating unnecessary variables.

Change the definition of Appointment to look as follows:

export const Appointment = ({ customer }) => (
  <div>{customer.firstName}</div>
);

Note that I haven't fully destructured this. I could have written this function like this:

export const Appointment = ({ customer: { firstName } }) => (
  <div>{firstName}</div>
);

The first version is no longer than the second; however, if you're counting tokens, it has one less set of curly braces. The most concise solution always wins!

Run tests; we expect this test to now pass, as follows:

PASS test/Appointment.test.js
 Appointment
 ✓ renders the customer first name (21ms)
 ✓ renders another customer first name (2ms)

Great work! We're done with our passing test.

 

Refactoring your work


The next step of the TDD cycle is to refactor your work. This step is often the hardest, because our natural impulse can be to get straight into the next feature. Chasing green, as I like to call it: building more and more functionality is much more exciting. Refactoring, however, is much more zen.

The rule "more haste; less speed" applies to coding, just as in many other areas of life. If you skip the refactoring phase, your code quality will deteriorate. If you develop a habit of skipping refactoring, your code base will soon become difficult to work with.

It takes a lot of personal discipline to consistently refactor, but you will reap the rewards of a code base that remains maintainable as it ages.

Right now, we have some repeated code between our two tests. Let's fix that.

Note

Test code needs as much care and attention as production code. The number one principle you'll be relying on when refactoring your tests is Don't Repeat Yourself (DRY). Drying up tests is a phrase all TDDers repeat often.

Promoting variables

Both of our tests use the same two variables: container and customer. We can pull up these declarations to the outer describe scope, but leave the definitions within the tests. Since we'll be splitting declaration and definition, that also means we'll need to use let instead of const:

Just above the first test, write the following two lines:

let container;
let customer;

Then, remove the word const from both of the tests, and re-run your tests, which should still be passing.

Using a beforeEach block

Both of our tests start with some setup or arrangement. When that setup is common to all tests, we can promote them into a beforeEach block instead. Code in this block is executed before each test.

Above your first test, write the following code, and delete the corresponding call to createElement from each of your two tests:

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

Since we defined container in the scope of the describe block, the value set here in the beforeEach block will be available to our test once it executes.

Note

Be careful when you use variables defined within the describe scope. These variables are not cleared between each test execution, so you are running the risk of non-independent tests. Therefore, any variable you declare in the describe scope should be assigned to a new value in a corresponding beforeEach block, or in the first part of each test, just as we've done here.

Extracting methods

The call to ReactDOM.render is the same in both methods. Since it's the same in both methods, it makes sense to pull it out.

However, rather than pull it out as-is, we can create a new function that takes the Appointment component as its parameter. This way, we can clearly see how our test data objects are woven through the object under test. If we hid that within an extracted method, the test would be less clear.

Note

The parts of a test that you want to see are the parts that differ between tests. Usually, some data remains the same (container in this example) and some differs (customer in this example). Do your best to hide away whatever is the same and proudly display what differs.

Above the first test, write the following definition:

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

Now, replace the call to ReactDOM.render in each test with this line:

render(<Appointment customer={customer} />);

Re-run your tests now—they should still be passing.

 

Writing great tests


The first test now looks like this:

it('renders the customer first name', () => {
  customer = { firstName: 'Ashley' };
  render(<Appointment customer={customer} />);
  expect(container.textContent).toMatch('Ashley');
});

This is concise and clearly readable.

A good test has three distinct sections:

  • Arrange: Sets up test dependencies
  • Act: Executes production code under test
  • Assert: Checks expectations are met

A great test is not just good but is also the following:

  • Short
  • Descriptive
  • Independent of other tests
  • Has no side-effects

Red, green, refactor

We’ve covered a lot of ground, and we have gone into excruciating detail for a very simple test. All of the ground work is now done for us to speed up.

Let's look at the red, green, refactor cycle:

The steps of the TDD cycle are as follows:

  1. Write a failing test: Write a short test that describes some functionality you want. Execute your test and watch it fail. If it doesn’t fail, then it's not a good test; go back and try again.
  2. Make it pass: Make the test green. Do the simplest thing that will work. Feel free to make a mess; you can clean it up later.
  3. Refactor your code: Stop, slow down, and resist the urge to move on to the next feature. Work hard to make your code—both production and test code—as clean as it can be.

Streamlining your testing process

Think about the effort you've put into this book so far. What actions have you been doing the most? Most likely, you've been doing these:

  • Switching betweensrc/Appointment.jsandtest/Appointment.test.js
  • Runningnpm test

To solve the first issue, you should use split-screen functionality in your editor. If you aren't already using that, then take this opportunity to learn how to do it. Load your production module on one side and the corresponding unit test file on the other. Here's a picture of my setup:

You can see that I also have a little test window at the bottom for showing test output.

Jest can also watch your files and auto-run tests when they change. To enable this, change the test command in package.json to jest --watchAll. This reruns all of your tests when it detects any changes.

Note

Jest has an option to run only the tests in files that have changed, but you’ll find that since your React app will be composed of many different files, each of which is interconnected, it's better to run everything, as breakages can happen in many modules.

 

Rendering lists and detail views


Note

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.

Note

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();
  });
});

Note

It isn't always necessary to wrap your component in adivwith 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 forAppointmentsDayView.

This test uses the exact samerenderfunction from the firstdescribeblock, as well as the samelet containerdeclaration andbeforeEachblock. 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 lastimportin your test fileto read as follows:
import {
  Appointment,
  AppointmentsDayView
} from '../src/Appointment';
  1. In src/Appointment.js, add this functional component below Appointment:
export const AppointmentsDayView = () => null;

Note

When we first defined ourAppointmentcomponent 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 toskip pastthe 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 AppointmentsDayViewdescribe 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)

Note

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 needol 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>
);

Note

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>

Note

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 thestartsAttimestamp for eachlikey. Let's just imagine that we have that test in place.

Specifying list items

Now, let's fill in the list items for the ol element we just rendered.

  1. Create a third test in the new describe block, with this content:
it('renders each appointment in an li', () => {
  const today = new Date();
  const appointments = [
   { startsAt: today.setHours(12, 0) },
   { startsAt: today.setHours(13, 0) }
  ];
  render(<AppointmentsDayView appointments={appointments} />);
  expect(container.querySelectorAll('li')).toHaveLength(2);
  expect(
    container.querySelectorAll('li')[0].textContent
  ).toEqual('12:00');
  expect(
    container.querySelectorAll('li')[1].textContent
  ).toEqual('13:00');
});

Jest will show the following error:

  Expected length: 2
  Received length: 0
  Received object: []

    58 | render(<Appointments appointments={appointments} />);
    59 | expect(container.querySelectorAll('li')).toHaveLength(2);
  > 60 |                                         ^
  1. Change that div element into an li element:

<ol>
  {appointments.map(appointment => (
    <li key={appointment.startsAt} />
  ))}
</ol>
  1. You'll see the following error from Jest:

expect(received).toEqual(expected)

Difference:

- Expected
+ Received

- 12:00
+

  59 | expect(container.querySelectorAll('li')[0].textContent)
> 60 |   .toEqual('12:00');
     |    ^
  61 | expect(container.querySelectorAll('li')[1].textContent)
  62 |   .toEqual('13:00');
  63 | });
  1. Add the following function to src/Appointment.js that converts a Unix timestamp (which we get from the return value from setHours) into a time of day. It doesn't matter where in the file you put it; I usually like to define constants before I use them, so this would go at the top of the file:
const appointmentTimeOfDay = startsAt => {
  const [h, m] = new Date(startsAt).toTimeString().split(':');
  return `${h}:${m}`;
}

Note

This uses the destructuring assignment and template literals, which are both space-saving features that you should start using if they aren't already part of your toolbox.TDD can help us to overcome the fear of using complicated language features. If we're ever unsure what production code does, we can simply look at the tests to tell us.

  1. Use the previous function to update AppointmentsDayView, as follows:
<ol>
  {appointments.map(appointment => (
    <li key={appointment.startsAt}>
      {appointmentTimeOfDay(appointment.startsAt)}
    </li>
  ))}
</ol>
  1. Running tests should show everything as green:

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 (13ms)
    ✓ renders each appointment in an li (4ms)
  1. This is a great chance to refactor. Both of ourAppointmentsDayViewtests use the same datasets. These can be lifted out into the describe scope, the same way we did with customer in theAppointmenttests.This time, however, they can remain asconstdeclarations as they never change. To do that, move the todayandappointmentsdefinitions from one of the tests to the top of the describe block, above beforeEach. Then, delete the definitions from both tests.

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:

  • Abuttonelement within ourli
  • AnonClickhandler that is attached to thatbuttonelement
  • 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 AppointmentsDayViewdescribe 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 divcomponent 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.

Note

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 eachlielement has abuttonelement:

  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 bebutton. If you haven't seen this before, it's idiomatic when using button elements to define its role by setting thetypeattribute, 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');
});

Note

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 theReactTestUtils.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');
});

Note

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.

Note

At this stage, I'm not too bothered about where the customer name appears on screen. Testing container.textContentis 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, usingit.skip.

Note

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 thisselectedAppointmentrather 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 onClickhandler on thebuttonelement:
<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

Note

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.

 

Adding an entrypoint

React applications are composed of a hierarchy of components that are rendered at the root. Our application entrypoint should render this root component.

I tend to not test-drive my entrypoint, because any test that loads our entire application can become quite brittle as we add in more and more dependencies into it. InSection 4, Acceptance Testing with BDD, we'll look at using acceptance tests to write some tests thatwillcover the entrypoint.

Since we aren't test-driving it, we follow a couple of general rules:

  • Keep it as brief as possible

  • Only use it to instantiate dependencies for your root component, and to callReactDOM.render.

Before we run our app, we'll need some sample data. Create a file named src/sampleData.js and fill it with the following:

const today = new Date();

const at = hours => today.setHours(hours, 0);

export const sampleAppointments = [
  { startsAt: at(9), customer: { firstName: 'Charlie' } },
  { startsAt: at(10), customer: { firstName: 'Frankie' } },
  { startsAt: at(11), customer: { firstName: 'Casey' } },
  { startsAt: at(12), customer: { firstName: 'Ashley' } },
  { startsAt: at(13), customer: { firstName: 'Jordan' } },
  { startsAt: at(14), customer: { firstName: 'Jay' } },
  { startsAt: at(15), customer: { firstName: 'Alex' } },
  { startsAt: at(16), customer: { firstName: 'Jules' } },
  { startsAt: at(17), customer: { firstName: 'Stevie' } }
];

Note

The GitHub repository contains a more complete set of sample data. You can use this by pulling the same file, src/sampleData.js, from the tag extracting-helpers.

This list also doesn't need to be test-driven, for a couple of reasons:

  • It's a list of static data with no behavior.
  • This module will be removed once we begin using our back-end API to pull data.

Note

Test-driven development is often a pragmatic choice. Sometimes, not test-driving is the right thing to do.

Create a new file src/index.js and enter the following:

import React from 'react';
import ReactDOM from 'react-dom';
import { AppointmentsDayView } from './Appointment';
import { sampleAppointments } from './sampleData';

ReactDOM.render(
  <AppointmentsDayView appointments={sampleAppointments} />,
  document.getElementById('root')
);

That's all you'll need.

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:

Note

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.

Before you check in...

Make sure to add dist/main.js to your .gitignore file. This file is generated by Webpack, and as with every generated file, you shouldn't check it in.

You may also want to add a README.md at this point, to remind yourself how to run tests and how to build the application.

 

Summary


One of the many wonderful things about test-driven development is that it’s teachable. Tests act like a safety harness in our learning: we can build little blocks of understanding, building on top of each other, up and up to ever-greater heights, without fear of falling.

In this chapter, you've learned a lot of the test-driven development experience: the red-green-refactor cycle, triangulation, and Arrange, Act, Assert. You've also learned some design principles such as DRY and YAGNI.

While this is a great start, the journey has only just begun. Coming up next, we'll look at test-driving React forms and building complex user interface designs with our tests acting as scaffold.

 

Exercises


Note

The Git tag for this section is chapter-1-exercises.

  • Rename Appointment.js and Appointment.test.js to AppointmentsDayView.js and AppointmentsDayView.test.js. While it's fine to include multiple components in one file if they form a hierarchy, you should always name the file after the root component for that hierarchy.
  • Complete the Appointment component by displaying the following fields on the page. You should use a table HTML element to give the data some visual structure. This shouldn't affect how you write your tests:
    • Customer last name, using the lastName field
    • Customer telephone number, using the phoneNumber field
    • Stylist name, using the stylist field
    • Salon service, using the service field
    • Appointment notes, using the notes field
  • Add a heading to Appointment to make it clear which appointment time is being viewed.
  • There is some repeated sample data. We've used sample data in our tests and we also have sampleAppointments in src/sampleData.js, which we used to manually test our application. Do you think it is worth drying this up? If so, why? If not, why not?
 

Further learning


About the Author

  • Daniel Irvine

    Daniel Irvine is a software consultant based in London. He is a member of the European software craft community and works with a variety of languages including C#, Clojure, JavaScript, and Ruby. He’s a mentor and coach for junior developers and runs TDD and XP workshops and courses. When he’s not working, he spends time cooking and practicing yoga.

    Browse publications by this author

Latest Reviews

(1 reviews total)
Très bon livre, précis et concis.

Recommended For You

React Design Patterns and Best Practices - Second Edition

Build modular React web apps that are scalable, maintainable and powerful using design patterns and insightful practices

By Carlos Santana Roldán
React Projects

Build cross-platform applications of varying complexity for the web, mobile, and VR devices using React tooling

By Roy Derks
React Material-UI Cookbook

Develop modern-day applications by implementing Material Design principles in React using Material-UI

By Adam Boduch
Learn React Hooks

Create large-scale web applications with code that is extensible and easy to understand using React Hooks

By Daniel Bugl