DEV Community

Cover image for How to write tests for React in 2020 - part 2
Kelvin Liang
Kelvin Liang

Posted on • Updated on

How to write tests for React in 2020 - part 2

Writing React Test with React recommend libraries - Jest & Testing Library for React Intermediate users.

Please Note

In this article, I will explore more advanced concepts in React Testing, I hope you find them helpful for your situations. If you are a beginner in React or new to testing, I would suggest you check out Part 1 Here to have some fundamental knowledge before continue, thanks!

First, let's look at the Accessibility Test.

Front-end development is all about visualization and interacting with end-users, Accessibility Test can ensure our apps can reach as many as possible users out there.

From React
From - https://reactjs.org/docs/accessibility.html

Writing Accessibility Test for every aspect of your app seems very intimidated, but thanks for Deque Systems - A company dedicated on improving software accessibility by offering Axe testing package freely available online, we can now easily leverage the expertise from many senior developers around the world by importing Jest-axe along with Jest Library to test a web app's accessibility.

npm install --save-dev jest-axe

or

yarn add --dev jest-axe

With the package install, we can add the Accessibility Test into a project like this:

// App.test.js
import React from 'react';
import App from './App';
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

describe('App', () => {
  test('should have no accessibility violations', async () => {
    const { container } = render(<App />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});

It will help to ensure your FrontEnd Development complies with the latest version of WCAG(Web Content Accessibility Guidelines). For example, if you assign a wrong role to your Navigation bar component,

// ./components/navBar.js
...
<div className="navbar" role='nav'>
   ...
</div>
...

It will alert you like below:

A11y test wrong role

Check out a List of WAI-ARIA Roles Here.

Replace nav with navigation role as below, the test will pass.

// ./components/navBar.js
...
<div className="navbar" role='navigation'>
   ...
</div>
...

As we can see above, this test will help ensure you follow the WCAG(Web Content Accessibility Guidelines) standard so your app can reach most of the people out there.

Second, adding a Snapshot Test.

Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly. -- From Jest

You can put the test on the entire app or one specific component. They can serve different purposes during the development cycle, you can either use Snapshot Test to ensure your app's UI doesn't change over time or compare the differences between the last snapshot with current output to iterate through your development.

Let's take the example of writing a test for the entire App to show you how to write a snapshot test.

// App.test.js
import React from 'react';
import App from './App';

import renderer from 'react-test-renderer';
...

describe('App', () => {
  ...

  test('snapShot testing', () => {
    const tree = renderer.create(<App />).toJSON();
    expect(tree).toMatchSnapshot();
  });

});

If this is the first time this test run, Jest will create a snapshot file(a folder "__snapshots__" will create as well) looks similar to this.

snapshot-file-tree

// App.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`App snapShot testing 1`] = `
<div
  className="App"
>
  <div
    className="navbar"
  >
    ....

With this test in place, once you make any change over the DOM, the test will fail and show you exactly what is changed in a prettified format, like the output below:

snapshot-test-error

In this case, you can either press u to update the snapshot or change your code to make the test pass again.

If you adding a snapshot test at the early stage of development, you might want to turn off the test for a while by adding x in front of the test, to avoid getting too many errors and slowing down the process.

 xtest('should have no accessibility violations', async () => {
   ...
  });

Third, let's see how to test a UI with an API call.

It is fairly common now a frontend UI has to fetch some data from an API before it renders its page. Writing tests about it becomes more essential for the Front End development today.

First, let's look at the process and think about how we can test it.

web-api-datafetch

  1. When a condition is met (such as click on a button or page loaded), an API call will be triggered;
  2. When data come back from API, usually response need to parse before going to next step (optional);
  3. When having proper data, the browser starts to render the data accordingly;
  4. On the other hand, if something goes wrong, an error message should show up in the browser.

In FrontEnd development, we can test things like below:

  • whether the response comes back being correctly parsed?
  • whether the data is correctly rendered in the browser in the right place?
  • whether the browser show error message when something goes wrong?

However, we should not:

  • Test the API call
  • Call the real API for testing

Because most of the time, API is hosted by the third party, the time to fetch data is uncontrollable. Besides, for some APIs, given the same parameters, the data come back may vary, which will make the test result unpredictable.

For testing with an API, we should:

web-api-datafetch-Test

  • Use Mock API for testing and return fack data
  • Use fake data to compare UI elements to see if they match

If you got the ideas, let's dive into the real code practice.

Let's say we want to test the following News page component, where it gets the news from getNews API call and render them on the browser.

// ./page/News.js
import React, { useState, useEffect } from 'react';
import getNews from '../helpers/getNews';
import NewsTable from '../components/newsTable';

export default () => {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [errorMsg, setErrorMsg] = useState('');
  const subreddit = 'reactjs';

  useEffect(() => {
    getNews(subreddit)
      .then(res => {
        if (res.length > 0) {
          setPosts(res);
        } else {
          throw new Error('No such subreddit!');
        }
      })
      .catch(e => {
        setErrorMsg(e.message);
      })
      .finally(() => {
        setLoading(false);
      });
  }, [])

  return (
    <>
      <h1>What is News Lately?</h1>
      <div>
        {loading && 'Loading news ...'}
        {errorMsg && <p>{errorMsg}</p>}
        {!errorMsg && !loading && <NewsTable news={posts} subreddit={subreddit} />}
      </div>
    </>
  )
}

First, let's create a __mocks__ folder at where the API call file located. (In our case, the API call file call getNews.js), create the mock API call file with the same name in this folder. Finally, prepare some mock data inside this folder.

mock API folder

Mock API file (getNews.js) should look sth like below -

// ./helpers/__mocks__/getNews.js
import mockPosts from './mockPosts_music.json';

// Check if you are using the mock API file, can remove it later
console.log('use mock api'); 

export default () => Promise.resolve(mockPosts);

Vs. Real API Call

// ./helpers/getNews.js
import axios from 'axios';
import dayjs from 'dayjs';

// API Reference - https://reddit-api.readthedocs.io/en/latest/#searching-submissions

const BASE_URL = 'https://api.pushshift.io/reddit/submission/search/';

export default async (subreddit) => {
  const threeMonthAgo = dayjs().subtract(3, 'months').unix();
  const numberOfPosts = 5;

  const url = `${BASE_URL}?subreddit=${subreddit}&after=${threeMonthAgo}&size=${numberOfPosts}&sort=desc&sort_type=score`;

  try {
    const response = await axios.get(url);
    if (response.status === 200) {
      return response.data.data.reduce((result, post) => {
        result.push({
          id: post.id,
          title: post.title,
          full_link: post.full_link,
          created_utc: post.created_utc,
          score: post.score,
          num_comments: post.num_comments,
          author: post.author,
        });
        return result;
      }, []);
    }
  } catch (error) {
    throw new Error(error.message);
  }
  return null;
};

As we can see from the above codes, a mock API call just simply return a resolved mock data, while a real API call needs to go online and fetch data every time the test run.

With the mock API and mock data ready, we now start to write tests.

// ./page/News.test.js
import React from 'react';
import { render, screen, act } from '@testing-library/react';
import { BrowserRouter as Router } from "react-router-dom";
import News from './News';

jest.mock('../helpers/getNews');  //adding this line before any test.

// I make this setup function to simplify repeated code later use in tests.
const setup = (component) => (
  render(
   // for react-router working properly in this component
  // if you don't use react-router in your project, you don't need it.
    <Router>
      {component}
    </Router>
  )
);

...

Please Note:

jest.mock('../helpers/getNews');

Please add the above code at the beginning of every test file that would possibly trigger the API call, not just the API test file. I make this mistake at the beginning without any notifications, until I add console.log('call real API') to monitor calls during the test.

Next, we start to write a simple test to check whether a title and loading message are shown correctly.

// ./page/News.test.js
...
describe('News Page', () => {
  test('load title and show status', async () => {
    setup(<News />);  //I use setup function to simplify the code.
    screen.getByText('What is News Lately?'); // check if the title show up
    await waitForElementToBeRemoved(() => screen.getByText('Loading news ...'));
  });
...
});

mock_api_first_test_pass

With the mock API being called and page rendering as expected. We can now continue to write more complex tests.

...
test('load news from api correctly', async () => {
    setup(<News />);
    screen.getByText('What is News Lately?');

    // wait for API get data back
    await waitForElementToBeRemoved(() => screen.getByText('Loading news ...'));

    screen.getByRole("table");  //check if a table show in UI now
    const rows = screen.getAllByRole("row");  // get all news from the table

    mockNews.forEach((post, index) => {
      const row = rows[index + 1];  // ignore the header row

       // use 'within' limit search range, it is possible have same author for different post
      within(row).getByText(post.title);  // compare row text with mock data 
      within(row).getByText(post.author); 
    })

    expect(getNews).toHaveBeenCalledTimes(1); // I expect the Mock API only been call once
    screen.debug(); // Optionally, you can use debug to print out the whole dom
  });
...

Please Note

 expect(getNews).toHaveBeenCalledTimes(1);

This code is essential here to ensure the API call is only called as expected.

When this API call test passes accordingly, we can start to explore something more exciting!

As we all know, an API call can go wrong sometimes due to various reasons, how are we gonna test it?

To do that, we need to re-write our mock API file first.

// // ./helpers/__mocks__/getNews.js
console.log('use mock api');  // optionally put here to check if the app calling the Mock API
// check more about mock functions at https://jestjs.io/docs/en/mock-function-api
const getNews = jest.fn().mockResolvedValue([]); 
export default getNews;

Then we need to re-write the setup function in News.test.js file.

// ./page/News.test.js
...
// need to import mock data and getNews function
import mockNews from '../helpers/__mocks__/mockPosts_music.json';
import getNews from '../helpers/getNews';
...
// now we need to pass state and data to the initial setup
const setup = (component,  state = 'pass', data = mockNews) => {
  if (state === 'pass') {
    getNews.mockResolvedValueOnce(data);
  } else if (state === 'fail') {
    getNews.mockRejectedValue(new Error(data[0]));
  }

  return (
    render(
      <Router>
        {component}
      </Router>
    ))
};
...

I pass the default values into the setup function here, so you don't have to change previous tests. But I do suggest pass them in the test instead to make the tests more readable.

Now, let's write the test for API failing.

// ./page/News.test.js
...
test('load news with network errors', async () => {
    // pass whatever error message you want here.
    setup(<News />, 'fail', ['network error']);
    screen.getByText('What is News Lately?');

    await waitForElementToBeRemoved(() => screen.getByText('Loading news ...'));
    screen.getByText('network error');

    expect(getNews).toHaveBeenCalledTimes(1);
  })
...

Finally, you can find the complete test code from here.

Please Note
They are just simple test cases for demonstration purposes, in the real-world scenarios, the tests would be much more complex. You can check out more testing examples from my other project here.


Final Lesson
Photo by ThisisEngineering RAEng on Unsplash

Final words

In this article, I followed the best practices Kent C. Dodds suggested in his blog post - Common mistakes with React Testing Library published in May 2020, in which you might find my code is slightly different from Test-Library Example (I think soon Kent will update the docs as well), but I believe that should be how we write the test in 2020 and onward.

I use both styled-component and in-line style in this project to make UI look better, but it is not necessary, you are free to use whatever CSS framework in react, it should not affect the tests.

Finally, Testing is an advanced topic in FrontEnd development, I only touch very few aspects of it and I am still learning. If you like me, just starting out, I would suggest you use the examples here or some from my previous article to play around with your personal projects. Once you master the fundamentals, you can start to explore more alternatives on the market to find the best fit for your need.

Here are some resources I recommend to continue learning:

Resources and Article I referenced to finished this article:

Special Thanks for Johannes Kettmann and his course ooloo.io.

I have learned a lot in the past few months from both the course and fellows from the course - Martin Kruger and ProxN, who help inspire me a lot to finish this testing articles.

Below are what I have learned

  • Creating pixel-perfect designs
  • Planning and implementing a complex UI component
  • Implement data fetching with error handling
  • Debugging inside an IDE
  • Writing integration tests
  • Professional Git workflow with pull requests
  • Code reviews
  • Continuous integration

This is the Final finishing project as the outcome.

Top comments (5)

Collapse
 
nickytonline profile image
Nick Taylor • Edited

Just an FYI. You can add the axe toHaveNoViolations matcher globally so you don’t have to import it in your tests. See github.com/thepracticaldev/dev.to/... for an example of this. Then just reference it in your jest config, e.g. github.com/thepracticaldev/dev.to/...

Looking forward to your next post!

Collapse
 
kelvin9877 profile image
Kelvin Liang

Thanks for sharing your tips, Nick.
Sorry, I am still pretty new to jest, so what you suggest here is - I can make

import { axe, toHaveNoViolations } from 'jest-axe';

As default in every test in the app, so I don't need to import it one by one, or you mean I don't need to the whole test at all?

test('should have no accessiblity violations', async () => {
    const { container } = render(<App />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
Collapse
 
nickytonline profile image
Nick Taylor • Edited

You would still need to import axe, but that's it, i.e.

import { axe } from 'jest-axe';

And in your test, things would stay the same. i.e.

test('should have no accessiblity violations', async () => {
    const { container } = render(<App />);
    const results = await axe(container);

    expect(results).toHaveNoViolations();
});

It's a small change, but it saves importing it in every file that requires a11y checks.

Looking forward to your next post!

Collapse
 
jkettmann profile image
Johannes Kettmann • Edited

Great writeup Kelvin! And thanks for mentioning the course. Really appreciated and I'm glad you liked it.

A quick note on snapshot testing: I was a big fan of them for a long time. But at some point, I realized that they often don't add a lot of value. What typically happens when you have a lot of snapshot tests is that you change some components, 20 snapshot tests fail, and at some point, you just start pressing "u" to update them without thoroughly checking the changes. In my last projects, the teams tended to migrate away from snapshot testing completely. Here's a nice blog post again by Kent that describes how snapshot tests can still be used effectively.

Collapse
 
kelvin9877 profile image
Kelvin Liang • Edited

Thanks for pointing this out and sharing your own experiences on that, Johannes, really appreciated and I totally agree with it.

However, I still think it would still be a good idea to introduce the test and show them how to do it in the simplest way. If anyone interested they can always find a way to learn more or they can move on the next topic with some basic knowledge to take away from.

Finally, I really like the discussion you bringing up here, not only other readers but also myself can learn a lot from it, Thanks you! (That is why I like to start writing down what I learn in the journey of learning to code)