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:
- Since
containerneeds to be accessed in thebeforeEachblock and each of the tests, we must declare it in the outerdescribescope. And since we’ll be setting its value in thebeforeEachblock, that also means we’ll need to useletinstead ofconst. Just above the first test, add the following line of code:let container;
- Below that declaration, add the following code:
beforeEach(() => { container = document.createElement("div"); document.body.replaceChildren(container); }); - Delete the corresponding two lines from each of your two tests. Note that since we defined
containerin the scope of thedescribeblock, the value set in thebeforeEachblock 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:
- Above the first test, write the following definition. Note that it still needs to be within the
describeblock because it uses thecontainervariable:const render = component => act(() => ReactDOM.createRoot(container).render(component) );
- Now, replace the call to
renderin each test with the following line of code:render(<Appointment customer={customer} />); - In the preceding step, we inlined the JSX, passing it directly into
render. That means you can now delete the line starting withconst 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" ); }); - 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.