Share
BLOG

Testing React Applications Using Jest and React Testing Library

Tags:
Summary:
Learn how to avoid bugs and gain confidence when refactoring code by writing tests for your React code using Jest, React Testing Library and a Test Driven Development approach.

Intro

In this post I’m going to discuss how to test React applications using jest and react-testing-library. 

What is unique about this approach or solution?

  • It provides confidence when refactoring code by avoiding regression bugs when adding new features.
  • It adds documentation to your code automatically. Tests cases are a good way to understand what the code does.
  • It helps other team members to understand what you did.
  • It helps to find bugs that you might not be able to see when running the app.

What is the business impact?

When writing tests, in the beginning, you may feel that development is slow. However, after a few weeks, the code will be highly maintainable and will allow for the ability to add / remove features with the confidence that new features won’t break the application. This saves time by avoiding regression bugs and making on-boarding of new team members to the project easier, as they can read the tests and easily understand what the code does.


But what should we use to test our React applications? A few months ago we tried using Jest with Enzyme to test our apps and we found it a bit tedious. That is because Enzyme gives us the tools to test the implementation of each component, which is not what we want. From my point of view, I understand that implementations should be checked in pull requests and code reviews, but not using unit tests. Of course, you can test it if you want.

For example, testing the implementation of a component using Enzyme could be to check if the componentDidMount of a component has been called.


it('calls componentDidMount', () => {
  jest.spyOn(App.prototype, 'componentDidMount')
  const wrapper = shallow(<App />)
  expect(App.prototype.componentDidMount.mock.calls.length).toBe(1)
})


In this case, we don’t want to check if componentDidMount was called (implementation); what we want is to check that our component has what we expected when it is ready.


For me, that means it doesn’t matter how it was done, what matters is how it works and how it is shown. When you buy a car you don’t check how the brakes work, you just press the brake and you expect the car to stop. We take the same approach to test our components with react-testing-library.

What is react-testing-library?

React-testing-library is a library developed by Kent C. Dodds, which uses the DOM Testing Library as its core, enabling us to query DOM nodes and check what it contains as well as interact with them (firing events for example). It’s part of the testing-library family, which means that there is also a testing library version for Vue, Angular, etc. Writing tests using this library is simple, but before we write test cases, let’s talk about TDD.

TDD: Test Driven Development

Often when we start to create a component, we just read our customer requirements, check the design, create a MyComponent.tsx (or jsx) file and start to write the implementation. Once we have finished, we start to create tests to check the functionality. 

That would be fine, but in most cases doesn’t give us a global vision of what we need to do and which cases we need to check in order to avoid bugs later.


Using TDD we will write the tests first, and then we will write the code to pass the tests. So a good rule of thumb is to check the customer requirements first, then write a test case for each requirement. You will notice that while you are writing test cases, you will discover other cases. This approach also forces you to consider boundary cases more carefully which will avoid issues later.


For example, let’s suppose that our customer wants the following simple feature:

  • An element with two inputs, a button and some text
  • It should sum the two values when they press the button and show the result 

Starting from that point and using TDD we start creating our tests file, our component file (empty) and write some tests cases that will check to make sure our customer requirements are accomplished:


Gist file: sum1.test.tsx


import Sum from './Sum';
describe('Sum', () => {
  it('should render', () => (
  ));
  it('should have two inputs to add the numbers', () => (
  ));
  it('should have a button to fire the sum action', () => (
  ));
  it('should have text to show the result', () => (
  ));
  it('should sum value1 and value2 and show the result', () => (
  ));
})



But …  this forces you to think about a few things before you write a line of code:

  • What if the values are not numeric?
  • What if there are not values in one or the other inputs when you click the button?
  • Should there be any initial text?
  • Should there be any initial values in the input?
  • Is there is a specific text to show in the button?

So at this point, you can talk with your client and ask those questions. In doing so, you clarify the task you have to accomplish and control the scope. At the same time maybe your customer will more thoroughly consider behavior he doesn’t want or add some more test cases.

Writing tests

So let's write the tests for our component using react-testing-library. If you create a project using create-react-app, just add @testing-library/react as a dependency and import it into your tests.

The main utility we have is render, which enables us to render our component. The second utility we need to use is cleanup, which unmounts React trees that were mounted with render before each test is run. The last utility we will use is fireEvent, which enables us to fire events. Pretty straight forward.


Gist file: sum2.test.tsx


import React from "react";
import { render, cleanup, fireEvent } from "@testing-library/react";
import Sum from "./Sum";

describe("Sum", () => {
  afterEach(cleanup) 
  
  it("should render", () => {
    const SumComponent = render(<Sum />);
    expect(SumComponent).toBeTruthy();
  });
});

After run npm run test:

Our test should be in red since we haven’t written the code yet. Let’s make our test pass:

Gist file: sum1.tsx


import React from "react";
const Sum = () => <div />;
export default Sum;


Now we need to test that our component has two inputs. We can do it using different approaches since react-testing-library gives us many ways to query the DOM. My favorite way is to query using test-id, that way it doesn’t depend on the style or the content of the element.


So let's write our test first:


Gist file: sum3.test.tsx


import React from "react";
import { render, cleanup, fireEvent } from "@testing-library/react";
import Sum from "./Sum";

describe("Sum", () => {

   it('should have two inputs to add the numbers', () => {
    const { getByTestId } = render(<Sum />);
    
    const firstInput = getByTestId("value1");
    const secondInput = getByTestId("value2");
    
    expect(firstInput).toBeTruthy();
    expect(secondInput).toBeTruthy();
  });
});


Using getByTestId, we can look for any element with the data-testid attribute that matches the value. So in our component, we will do the following:


Gist file: sum2.tsx


import React from "react";

const Sum = () => (
  <div>
    <input data-testid="value1" />    
    <input data-testid="value2" />
  </div>
);
export default Sum;



Now we can do the same with all the other components. Notice that at this point, style and behavior do not matter. We are just testing that the component does what our client wants. We simply create  tests that check the behavior, to do that we will use fireEvent and extend our expects to check the text content by importing “jest-dom/extend-expect”:


Gist file: sum4.test.tsx


import React from "react";
import { render, cleanup, fireEvent } from "@testing-library/react";
import Sum from "./Sum";
import "jest-dom/extend-expect";

describe("Sum", () => {
  it('should sum value1 and value2 and show the result', () => {
    const { getByTestId } = render(<Sum />);
    
    const firstInput= getByTestId("value1");
    fireEvent.change(firstInput, { target: { value: 1 } } );
    
    const secondInput= getByTestId("value2");
    fireEvent.change(secondInput, { target: { value: 1 } } );
    
    const sumBtn = getByTestId("sum-button");
    fireEvent.click(sumBtn);
    
    const result = getByTestId("result-txt");
    expect(result).toHaveTextContent(2);
  });
});


Then we update our component:


Gist file: sum3.tsx


import React, { useState } from "react";
const Sum = () => {
  
  const [value1, setValue1] = useState();
  const [value2, setValue2] = useState();
  const [result, setResult] = useState();
  
  const calculateSum = () => setResult(value1 + value2);
  
  return (
    <div>
     <input 
       data-testid="value1" 
       value={value1}
       onChange={e => setValue1(e.target.value)}
     />
     <input 
       data-testid="value2" 
       value={value2}
       onChange={e => setValue2(e.target.value)}
     />
     <button data-testid="sum-button" onClick={calculateSum} /> 
     <p data-testid="result-txt">{result}</p>
    </div>
   );
};

export default Sum;



But hey, unexpected value!

As you may have noticed, what we are doing is concatenating strings and not summing numeric values. Let’s quickly fix that by adding parseInt when calling the setValue



And now our tests pass!

So we haven’t checked the UI, but we can be sure that 1 + 1 equals 2.

But what if we try to sum decimals? 1.5 + 1.5 should be equal to 3 but …

That is because we used parseInt. Let’s change it to parseFloat and now our tests will pass:

This is a very basic example, but it shows how the automated testing approach helped us get the component to behave as we need it to by testing for edge cases and appropriately handling data types.


Here is the Codepen that shows our simple example: https://c8sz2.codesandbox.io/

Tips when testing your components

  1. Write pure functions and move them outside the component. Doing so will make them easier to test.
  2. Try to split your code into smaller functions and components. It’s easy to test small pieces and then create complex structures using them because that gives you the confidence that it will work as all the small pieces have been tested. Divide and conquer!
  3. Try to reach 100% coverage. That will help you to find all the edge cases.
  4. Add edge cases to your test, try to break your component.

Debugging your tests

Tests are code too! Sometimes they will need to be debugged as well.  Some IDEs have it integrated but, in this example, we’ll use chrome.

  1. Add this script to debug tests in your package.json:
    "test:debug": "node --inspect node_modules/.bin/jest --runInBand"
  2. Add a debugger statement wherever you want to stop the test execution and inspect values
  3. Open chrome and go to chrome://inspect and open the dedicated Dev tools for Node:

  1. Run the script. You will see that the execution will stop in your debugger sentence and you will be able to inspect the values.

If you have questions about this blog, shoot me an email <info@experoinc.com>. Happy testing!




Gist URL (Please fork it from the expero blog account):

https://gist.github.com/ivanbtrujillo/da26337cd63446e58d745ca4dd310ebd


Subscribe to our quarterly newsletter

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.