Unit Testing React Apps
So far we have defined a specification for our app. We have empty components that could fullfill the requirement of that specification. Now we will write tests that our components will need to pass in order to prove they meet that specification.
Unit Testing React Components With React Test Renderer
Install React Test Renderer
yarn add react-test-renderer
Find Props
//DO NOT test like this.test("Component renders value", () => {const value = "The Value";const testRenderer = TestRenderer.create(<DisplayValue value={value} />);//Get the rendered nodeconst testInstance = testRenderer.root;//find the div and make sure it has the right textexpect(testInstance.findByType("div").props.children).toBe(value);});
Do This For Every Prop?
FALSE. That Is Testing React, Not Your Application
Snapshot Testing
Instead of manually searching and testing every single prop. We can use snapshot tests.
Snapshots acomplish two things:
- Make sure your props went to the right places.
- Force your to commit to changes.
Snapshot tests render components to JSON and store that JSON representation in the file system. If a future test run produces a different JSON string, the test fails.
This allows you to commit to the output of a component, given a specific input. If a change to the code causes a snapshot to fail, this tells you one of two things. It could tell you that you have created a regression error. This tells you to go back and fix what you accidently broke. Aleternatively it could confirm that the change to the functionality of the component that you wanted had happened.
For example, if you are assigned to change the class attribute of the input generated by a component, you would expect the diff to show a change in the snapshot reflecting the class attribute had changed. In code review, you can read snapshots to ensure they match the expected behaviour of a component, and what changes are being committed to. Or you may see changes to the snapshot that indicate undesired changes.
Create A Snapshot Test
In the test file for the <DisplayValue>
component, remove those messy tests from last time and replace it with a snapshot test:
test("Component renders correctly", () => {expect(TestRenderer.create(<DisplayValue value={"The Value"} className={"the-class-name"} />).toJSON()).toMatchSnapshot();});
This is less code, and it uses the component in the same way we will use it in the actual app. Unit tests are always contrived, but the more that reflects how the component is used, the more valuable the test.
Unit Testing React Apps With React Testing Library
Our <EditValue />
component will be provided a value and a function that fires when that value is changed and returns the new value.
Testing React Events
We could use React Test Render's act()
function for these tests. Instead, we'll use a higher-level tool tool that takes away most of the complexity of interacting with rendered React components. We'll use React Testing Library. Testing Library's React implimentation allows us to select nodes from the rendered app, like a user would, and make assertions about them or take actions, such as fireing change events, with them.
Install React Testing Library
yarn add @testing-library/react
Testing Change Events With React Testing Library
We can use React Testing Libraray to test change events in the <EditValue />
component. In these tests, instead of passing a real change handler, we will use Jest's mock function instead.
jest.fn()
are useful helpers for testing if the component interacted with the onChange function correctly. Like all mocks, they help isolate components during unit testing. Later in integration tests, we will make sure that the real onChange function behaves accordingly. Using Jest's mock utility, we can learn about how the function was called. That's what our unit test is concerned with -- how the unit being tested uses the callback. We are not concerned with what the callback does in this test. That's a seperate unit of functionality.
Setting Up Tests With React Testing Library
Let's look at how to setup our tests for <EditValue>
with React testing library. We're still using the BDD-style with nesting from the last time we looked at this test file. Now, I've used the afterEach()
function from Jest to run a function that resets the DOM instance we're testing with after each test. This way each unit test is isolated from each other.
//import component to testimport { EditValue } from "./EditValue";import {render, //test renderercleanup, //resets the JSDOMfireEvent //fires events on nodes} from "@testing-library/react";'describe("EditValue component", () => {afterEach(cleanup); //reset JSDOM after each testit("Calls the onChange function", () => {//put test here});it("Passe the right value to onChange", () => {//put test here});});
Simulating A Change Event With React Testing Library
In this test, we will render the component, as it is used in the app. We can deconstruct the function getByLabelText from the result of the render. We can use that to find the input the same way a user would -- by reading the label test. One hidden advantage of this is that all inputs have to have a label, which is required for the app to be accesible to screen readers.
it("Calls the onChange function", () => {//mock function to test withconst onChange = jest.fn();//Render component and get back getByLabelText()const { getByLabelText } = render(<EditValueonChange={onChange}value={""}className={"some-class"}label={'Edit Value'}/>);//Get the input by label textconst input getByLabelText('Edit Value');//Fire a change event on the inputfireEvent.change(input, {target: { value: "New Value" }});//Was callback function called once?expect(onChange).toHaveBeenCalledTimes(1);});
In this test, we render the component and then find the input by label. At this point, if the component does not have the correct structure, those lines will error out. Then we use the input -- if it exists -- to fire a change event. The actuall assertions only run if all of that actually works. Beacuse we're using the component like an end user, the errors generated by running these lines should be useful in building or fixing our component.
In this test, the assertion ensures that the callback is called once. If it's not called at all, that means the onChange
callback is probably not bound properly. If it gets called more than once, that indicates a bug, probably a bad race condition.
This test does not prove that the right value is passed to the onChange
prop. I want to pass a string, not the event object:
it("Passes the right value to onChange", () => {const onChange = jest.fn();const { getByLabelText } = render(<EditValueonChange={onChange}value={""}className={"some-class"}label={'Edit Value'}/>);const input getByLabelText('Edit Value');//Fire a change event on the inputfireEvent.change(input, {target: { value: "New Value" }});//Was the new value -- not event object -- sent?expect(onChange).toHaveBeenCalledWith("New Value");});
This test is almost identical. One school of thaught says one assetrtion per unit, that way you know exactly what a failure indicates. This is following that rule. Beacuse the first argument of it()
is a description of the test, it makes reading test results really nice.
Also, it's a ton of code duplication. A less orthodox approach says test the interaction toghether. For integration or acceptance tests, I think that's super valid. For unit tests like this, I like this way. That is a very loosley held opinion.
Snapshot Testing With React Testing Library
We can also use React testing library for snapshot testing. It's very simple:
it("matches snapshot", () => {expect(render(<EditValueonChange={jest.fn()}value={"Hi Roy"}id={"some-id"}className={"some-class"}/>)).toMatchSnapshot();});
Discovering Component API
So far we have tests, but not components. In the tests, we're assuming props these components do not have. That's intentional. The tests show how the component will have to work. The tests are showing us how the components should work. We're learning how to write the components from the tests. That's a SOLID strategy.
We can not be sure that the props are correct until we see how the components integrate toghether. Integration tests, which use the same tools as unit tests, in different ways are next.