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
container
needs to be accessed in thebeforeEach
block and each of the tests, we must declare it in the outerdescribe
scope. And since we’ll be setting its value in thebeforeEach
block, that also means we’ll need to uselet
instead 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
container
in the scope of thedescribe
block, the value set in thebeforeEach
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:
- Above the first test, write the following definition. Note that it still needs to be within the
describe
block because it uses thecontainer
variable:const render = component => act(() => ReactDOM.createRoot(container).render(component) );
- Now, replace the call to
render
in 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.