Book Image

Mastering React Test-Driven Development - Second Edition

By : Daniel Irvine
Book Image

Mastering React Test-Driven Development - Second Edition

By: Daniel Irvine

Overview of this book

Test-driven development (TDD) is a programming workflow that helps you build your apps by specifying behavior as automated tests. The TDD workflow future-proofs apps so that they can be modified without fear of breaking existing functionality. Another benefit of TDD is that it helps software development teams communicate their intentions more clearly, by way of test specifications. This book teaches you how to apply TDD when building React apps. You’ll create a sample app using the same React libraries and tools that professional React developers use, such as Jest, React Router, Redux, Relay (GraphQL), Cucumber, and Puppeteer. The TDD workflow is supported by various testing techniques and patterns, which are useful even if you’re not following the TDD process. This book covers these techniques by walking you through the creation of a component test framework. You’ll learn automated testing theory which will help you work with any of the test libraries that are in standard usage today, such as React Testing Library. This second edition has been revised with a stronger focus on concise code examples and has been fully updated for React 18. By the end of this TDD book, you’ll be able to use React, Redux, and GraphQL to develop robust web apps.
Table of Contents (26 chapters)
1
Part 1 – Exploring the TDD Workflow
10
Part 2 – Building Application Features
16
Part 3 – Interactivity
20
Part 4 – Behavior-Driven Development with Cucumber

Refactoring your work

Now that you’ve got a green test, it’s time to refactor your work. Refactoring is the process of adjusting your code’s structure without changing its functionality. It’s crucial for keeping a code base in a fit, maintainable state.

Sadly, the refactoring step is the step that always gets forgotten. The impulse is to rush straight into the next feature. We can’t stress how important it is to take time to simply stop and stare at your code and think about ways to improve it. Practicing your refactoring skills is a sure-fire way to level up as a developer.

The adage “more haste; less speed” applies to coding just as it does in life. If you make a habit of skipping the refactoring phase, your code quality will likely deteriorate over time, making it harder to work with and therefore slower to build new features.

The TDD cycle helps you build good personal discipline and habits, such as consistently refactoring. It might take more effort upfront, but you will reap the rewards of a code base that remains maintainable as it ages.

Don’t Repeat Yourself

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.

The key point is that you want your tests to be as concise as possible. When you see repeated code that exists in multiple tests, it’s a great indication that you can pull that repeated code out. There are a few different ways to do that, and we’ll cover just a couple in this chapter.

You will see further techniques for drying up tests in Chapter 3, Refactoring the Test Suite.

Sharing setup code between tests

When tests contain identical setup instructions, we can promote those instructions into a shared beforeEach block. The code in this block is executed before each test.

Both of our tests use the same two variables: container and customer. The first one of these, container, is initialized identically in each test. That makes it a good candidate for a beforeEach block.

Perform the following steps to introduce your first beforeEach block:

  1. Since container needs to be accessed in the beforeEach block and each of the tests, we must declare it in the outer describe scope. And since we’ll be setting its value in the beforeEach block, that also means we’ll need to use let instead of const. Just above the first test, add the following line of code:
    let container;
  2. Below that declaration, add the following code:
    beforeEach(() => {
      container = document.createElement("div");
      document.body.replaceChildren(container);
    });
  3. Delete the corresponding two lines from each of your two tests. Note that since we defined container in the scope of the describe block, the value set in the beforeEach block will be available to your test when it executes.

Use of let instead of const

Be careful when you use let definitions within the describe scope. These variables are not cleared by default between each test execution, and that shared state will affect the outcome of each test. A good rule of thumb is that 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.

For a more detailed look at the use of let in test suites, head to https://reacttdd.com/use-of-let.

In Chapter 3, Refactoring the Test Suite, we’ll look at a method for sharing this setup code between multiple test suites.

Extracting methods

The call to render is the same in both tests. It’s also quite lengthy given that it’s wrapped in a call to act. It makes sense to extract this entire operation and give it a more meaningful name.

Rather than pull it out as is, we can create a new function that takes the Appointment component as its parameter. The explanation for why this is useful will come after, but now let’s perform the following steps:

  1. Above the first test, write the following definition. Note that it still needs to be within the describe block because it uses the container variable:
    const render = component =>
      act(() => 
        ReactDOM.createRoot(container).render(component)
      );
  2. Now, replace the call to render in each test with the following line of code:
    render(<Appointment customer={customer} />);
  3. In the preceding step, we inlined the JSX, passing it directly into render. That means you can now delete the line starting with const component. For example, your first test should end up looking as follows:
    it("renders the customer first name", () => {
      const customer = { firstName: "Ashley" };
      render(<Appointment customer={customer} />);
      expect(document.body.textContent).toContain(
        "Ashley"
      );
    });
  4. Rerun your tests and verify that they are still passing.

Highlighting differences within your tests

The parts of a test that you want to highlight are the parts that differ between tests. Usually, some code remains the same (such as container and the steps needed to render a component) and some code differs (customer in this example). Do your best to hide away whatever is the same and highlight what differs. That way, it makes it obvious what a test is specifically testing.

This section has covered a couple of simple ways of refactoring your code. As the book progresses, we’ll look at many different ways that both production source code and test code can be refactored.