TDD in React
This blog post assumes the following has been installed on your machine.
You will have the following in place at the end of this post:
- A simple React application with two components using the create-react-app node package
- Unit tests
- Smoke testing
- Isolated (shallow) testing
- Snapshot testing
- Integrated testing
- Code coverage
The resulting project can be found HERE.
Setup a React application
There is a great Github repo “create-react-app”, which can be run using npx. The great thing about npx is that it doesn’t download and install the create-react-app package globally. This package is used as a scaffolding tool in order to create the React application. We don’t want it to be there after the application has been created and we always want to create the application with the latest version of the scaffolding tool. See NPX for more features.
To do this, specify the following command:
C:\Users\username\temp>npx create-react-app my-app
The folder structure will then look as follows:
The application comes with some default functionality and is a good starting point for a TDD approach. Test files can be both named with the .text or .spec postfix.
Let’s open App.test.js:
At the top, React is needed as the front end JS library. “ReactDOM” contains methods regarding the DOM that can be used at the top level of a React application. The third import statement makes sure that the “App” component is available for testing.
There is a default smoke test that basically renders the App component inside a div element and assumes no errors occur during the rendering. The unmountComponentAtNode is needed to clean up the DOM and remove the div and everything below. This method returns true if the component has been successfully unmounted so it can be wrapped in an expect statement as follows:
ReactDOM should only be used for functionality, not for testing purposes. I added it because the team from “create react app” includes it in the test file. The general advice is to use React test renderer or Enzyme. The statement from the team on this can be found here.
When running the tests (through the “npm test” command), the following is shown:
In Visual Studio Code, the Jest extension by Orta is very useful as it shows you (among many other features) which tests have failed inside the test files.
Let’s create some unit tests for a simple button. This button should be a React component of which the title can be set.
I used this React style guide to set up the folder structure.
From the previous section, we know how we can create a smoke test using React Dom. Now, let’s do some shallow testing. Shallow testing is used to test components in isolation. This means that if component A renders HTML, component B, and component C, that we only want to test the HTML. Not the nested components B and C. To test those, there should be shallow tests of those components (B.test.js and C.test.js). As we will see later, integrated component testing is useful when we want to test A, B, and C.
Enzyme is a great library to do shallow testing and we need to install it first:
npm install --save-dev enzyme enzyme
In the package.json file we need to include the setupTests.js file:
The shallow test in Button.test.js is defined as follows:
Enzyme is used to render a shallow of the Button component. This means that if the Button would have other React children components, these would not be fully rendered. Shallow testing is useful to create isolated tests.
There is a line in the test report that mentions zero snapshots. In the next section, I will explain snapshot testing.
Snapshot testing is a technique that verifies the integrity of rendered components. This means that the output HTML is watched for changes and a warning is displayed if changes are made. When a snapshot test is written, the output HTML is written to a file (a snapshot is taken) and is used as a baseline. After subsequent edits on the component, the edits will be compared against the snapshot file and warnings are displayed if there are differences.
The newly rendered button does not match the snapshot anymore and we are notified by this.
The button will need to be enriched with a text property so that a parent component can pass this dynamically:
Let’s change the render function of the button:
This allows the button to render text passed as a property by another component or (by default) display “OK”. The tests should pass.
Next, the App component will contain the Button component and pass the text “Press” to it. We expect the html to contain the Button with the text “Press”:
At the top of App.test.js:
The render unit test:
Notice the “mount” keyword here. As opposed to “shallow” it fully renders the component (including child components). The test will fail so let’s add functionality to App.js that makes it pass.
At the top:
The render function:
This is a good example of when code coverage does not tell you as much as you would like. The App and Button are covered 100% by one of the three rendering function (ReactDOM, React test renderer or Enzyme mount). This triggers the render function of the component and since this is all code in it, it will report 100%.
ReactDOM, React test renderer and Enzyme
For me it was confusing to know when to use ReactDOM, the react test renderer and Enzyme.
- ReactDOM (LINK)
- Used to write functionality, not for testing
- Interact with the DOM directly, so a DOM is needed
- React test renderer (LINK)
- Used to write unit tests
- Used to do shallow (LINK), snapshot and integrated testing
- Enzyme (LINK)
- Used to write unit tests
- Used to do shallow, snapshot and integrated testing
- Contains a lot of helper functions that make for example DOM manipulation easier
- Uses the React test renderer
From a TDD with React perspective, the React test renderer and Shallow renderer would have sufficed. I still believe it is good to add Enzyme at an early stage since most projects will be more complex then this example. Having a testing utility ready in advance eases development. I am not sure why the spec file that comes with the create react app uses the ReactDOM. In my opinion it should use the React test renderer or Enzyme (mount).