It seems like TDD, or test driven development is shrouded in confusion for many developers out there because it can be seen to be complex and scary.
I want to say right away that this is not the case, TDD/test driven development, is actually very simple, in my opinion, even more so than just writing react components without it because TDD will pretty much tell you what you need to do.
It is just a coding pattern or process that enables you to make sure that all of your code is well tested, and the code is all 100% necessary.
What is test driven development, or TDD?
Test driven development is a programming pattern where you write tests before the code you are testing. You write a test which should fail at first, you then write the minimum code possible to make it pass, you then repeat this for the next logical bit of functionality you need, and so on until your component matches your specification or desired outcome. This usually means the component is fully tested from each angle.
With the test driven development coding pattern you simply write each test before you write the equivalent code that will make that test pass.
Using TDD with React
Let’s go through a quick example to help understand how we can use TDD to create a simple component.
For this example I have used create-react-app
to generate a simple project.
Now let’s say you want to create a new component that will render hello world
inside of a h1
, instead of first creating the component like you usually would, let’s create the test file first and add in a basic test.
HelloWorld.test.js
import React from 'react';
import ReactDOM from 'react-dom';
import HelloWorld from './HelloWorld';
describe('HelloWorld', () => {
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<HelloWorld />, div);
ReactDOM.unmountComponentAtNode(div);
});
});
Now if you were to run this test you would probably see some sort of error and then a failed test.
This is exactly what you would want and expect to see, it is how we know that are test is working. We know this because when the test fails it means that we know that it is catching the error we want it to.
Now we know that our test is working, let’s add the minimum possible code to our component, we will start by creating the file and adding a function because that is what our failing test needs.
HelloWorld.js
export default function HelloWorld() {
return null;
}
As you can see this component does not do very much right now, but if we run our test it is enough to make it pass, which is the only thing we need to worry about when practicing TDD.
Next we need to ask ourselves, does this component now do what we want it to, or does it match the spec? If the answer is yes, that’s our work done, however if the answer is no we need to add another test.
As I said before I wanted this component to return a h1
that says hello world
, in its current state the component only returns null, looks like we need to add another test!
Taking a look at what we need to do will provide us with our next test, in this case the next thing in the spec would be for it to return a h1
.
The first thing you might think about is how we are going to have to look at the output of our component and see if there is a h1
inside of it.
So now might be a good time to look for some functions/helpers that make it easy for us to do so.
Let’s take a quick look at the Official React Docs to see if anything has been recommended.
The React docs are recommending the use of React Testing Library
, so for simplicity let’s add that into our project by typing yarn add -D @testing-library/react
and get started with our next test.
(Just a note, I won’t be explaining the react testing library any further in this post as it does not directly relate to TDD, however if you want to read more there it has some great documentation).
HelloWorld.test.js
import React from 'react';
import ReactDOM from 'react-dom';
import { render } from '@testing-library/react';
import HelloWorld from './HelloWorld';
describe('HelloWorld', () => {
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<HelloWorld />, div);
ReactDOM.unmountComponentAtNode(div);
});
it('renders a h1', () => {
const { container } = render(<HelloWorld />);
expect(container.firstChild.nodeName).toEqual('H1');
});
});
Here we have added in a new test to check if our component is rendering a H1
element, now we have our next test we need to run yarn test
to see the outcome.
If the test now fails because we are getting null
instead of H1
, great that means our test is working and catching the error.
So next action would be to add in the <h1>
component like so:
HelloWorld.js
export default function HelloWorld() {
return <h1></h1>;
}
Do you think our test will now pass?
It should actually create an error when the test suit is run because we are trying to use JSX without importing React.
Now that we know that there is an error, and that it is already being caught by our test suite we don’t need to add in another test so we can work on the solution instead, which is to simply import react.
HelloWorld.js
import React from 'react';
export default function HelloWorld() {
return <h1></h1>;
}
Success, our tests are now all passing!
Once again, let’s ask ourselves is this what we want our component to do/ does it meet the specification requirements? The answer is still no, because we are expecting it to also contain the text hello world
.
Let’s add in a test for that:
HelloWorld.test.js
import React from 'react';
import ReactDOM from 'react-dom';
import { render } from '@testing-library/react';
import HelloWorld from './HelloWorld';
describe('HelloWorld', () => {
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<HelloWorld />, div);
ReactDOM.unmountComponentAtNode(div);
});
it('renders a h1', () => {
const { container } = render(<HelloWorld />);
expect(container.firstChild.nodeName).toEqual('H1');
});
it('renders a h1 with the text "hello world"', () => {
const { container } = render(<HelloWorld />);
expect(container.firstChild.nodeName).toEqual('H1');
expect(container.firstChild.textContent).toEqual('hello world');
});
});
Now we need to run our tests again to double check that they now fail. After it does let’s add in the bare minimum code to make the test pass.
HelloWorld.js
import React from 'react';
export default function HelloWorld() {
return <h1>hello world</h1>;
}
Finally we need to run the test suite one more time to confirm that our change has made our tests pass, which in this case it has.
At this point we have reached a point where our component is producing what we originally wanted it to and is matching the specification requirements (a h1
with hello world
).
This means we have now successfully created a finished component using test driven development in React!
Summary
Now you know the basics and idea of TDD, it is simply a case of putting it into use where you can.
You don’t have to use it throughout a whole project to start off with, in fact I would recommend just trying it out with a component or two to start off with.
There are some trade offs for using TDD, such as it extending the amount of time it takes to develop a component initially, but it also has many benefits.
To name a few benefits of TDD:
- In the future when you make changes to the component (which we all know is a certainty) the development time for these changes will be dramatically reduced because there will be less of a need for manual testing because you will know from your automated unit tests if the component is broken or is still working as expected.
- Most of the time it will deliver you 100% test coverage for that component.
One last thing to bear in mind, when you write your tests, try to write them based on the expected outcome of your component rather than testing the component itself, for example, try to avoid testing the state directly or avoid testing the exact elements used (unless they are important to accessibility or SEO).
Instead focus on the outcome, a quick example of this might be if you have a toggle box, rather than testing the state to see if a boolean value has switched to true or false to see if it is visible or not, check to see if the content is visible or not.