Testing React Applications with React Testing Library

Testing React Applications with React Testing Library

Testing is an essential part of software development, it helps ensure we ship quality software that works as expected. There’s 3 types of testing:

  • Unit testing: carried out on individual components of an application
  • Integration testing: combines components and tests their interactions with each other
  • End to end testing: carried out to ensure the flow of an application from start to finish goes as expected

In this article we’ll be looking at how we can carry out unit testing on React applications using React Testing Library.

What is React Testing Library?

React Testing Library is a library built on top of DOM Testing Library with APIs allowing it to work with React components. It aims to allow developers to write maintainable tests for React components by avoiding including implementation details of the components and focus and the actual tests. The idea behind this is to minimize breaking of tests when components are refactored.

React Testing Library is also very light-weight and encourages better testing practices with its primary guiding principle being:

The more your tests resemble the way your software is used, the more confidence they can give you.

Instead of working with rendered instances of React components, it works with actual DOM nodes. React Testing Library essentially acts as a replacement for Enzyme and uses Jest as a default test runner.

To showcase how React Testing Library we’ll build a an application consuming the Chuck Norris Jokes API building an app that fetches jokes from the API by category.

Setting up our app

We’ll be setting up our application with create-react-app if you don’t have it installed yet, you ca do this by running:

bash
npm install -g create-react-app$

Now let’s initiate our application by running:

bash
create-react-app joke-norris$

Yarn is the default package manager for create-react-app , you will need to have it installed if you don’t already do. You can do this by running:

bash
npm install -g yarn$

Inside the src directory of the application create three folders, components , redux and sass . These will hold our React components, redux files and SASS styling. We’ll also be using Semantic UI React as our UI framework to speed up building of our components. To install it, run:

bash
yarn add semantic-ui-react$

Our state management tool will be [redux](https://redux.js.org/, to install it and it’s dependencies run:

bash
yarn add redux react-redux redux-thunk redux-devtools-extension$

Creating the React Components

Now let’s create our first component, our titlebar/header, create a file called TitleBar.jsx in it place the following code:

jsx
import React, { Component } from 'react'; import { Header, Image } from 'semantic-ui-react'; import logo from '../logo.png' export class TitleBar extends Component { render() { return ( <Header textAlign='center' size='large' dividing > <Image style={{ display: 'block' }} centered circular src={logo} /> <Header.Content ><a href="/" className='app-title'>Joke Norris</a></Header.Content> </Header> ) } } export default TitleBar;

Next let’s create a component that will fetch and display categories of jokes from the API. Create a file called Categories.jsx and place the following code:

jsx
import React from 'react'; import { connect } from 'react-redux'; import * as categoryActions from '../redux/actions/categoryActions'; import { bindActionCreators } from 'redux'; import { Card, Loader, Header } from 'semantic-ui-react'; export class Categories extends React.Component{ componentDidMount(){ this.props.fetchCategories(); } renderCard = (category, index) => { const categoryUrl = `/categories/${category}` return( <div className='categories__Card' key={index}> <Card href={categoryUrl} color='green' raised > <Card.Content> <Card.Header content={category} style={{ textTransform : 'capitalize' }}/> </Card.Content> </Card> </div> ) } mapCategories = categories => { if (categories.length > 0){ return categories.map((category, index) => { return this.renderCard(category, index); }) } else { return ( <div> Categories Not Loaded </div> ) } } renderLoader = () => <Loader active={this.props.loading} inline /> render(){ const { categories, loading } = this.props; return( <div className='u-center-text'> <Header size='medium' color='grey' >Categories</Header> <p className='u-center-text'> Chuck Norris demands you choose a category </p> <div className='categories'> { (loading ? this.renderLoader() : this.mapCategories(categories) ) } </div> </div> ) } } Categories.defaultProps = { loading : true, } const mapStateToProps = (state, ownProps) => { return { categories : state.categories.data, loading : state.categories.loading, error : state.categories.error, } } const mapDispatchToProps = dispatch => { return bindActionCreators(categoryActions, dispatch) } export default connect(mapStateToProps, mapDispatchToProps)(Categories);

Now let’s create a component that renders a single joke from a certain category once the data is fetched. Create a SingleJoke.jsx file and initialize a React component as shown below:

jsx
import React, { Component } from 'react'; import { connect } from 'react-redux'; import * as jokeActions from '../redux/actions/jokeActions'; import { bindActionCreators } from 'redux'; import { Loader, Segment, Label, Button, Icon } from 'semantic-ui-react'; export class SingleJoke extends Component { componentDidMount(){ const category = this.props.match.params.category this.props.fetchJoke(category); } refresh = () => { window.location.reload(); } renderJoke = joke => { const category = this.props.match.params.category if (joke){ return( <Segment raised key={joke.id}> <Label as='a' color='red' ribbon size='massive' style={{ textTransform : 'uppercase'}}> {category} </Label> <div > <div> <p className='joke'>{joke.value}</p> </div> </div> <Button primary icon labelPosition='right' href='/'> <Icon name='home' /> Choose Category </Button> <Button positive color='teal' icon labelPosition='right'onClick={this.refresh}> Next <Icon name='right arrow' /> </Button> </Segment> ) } else { return ( <div> Joke Not Loaded </div> ) } } renderLoader = () => <Loader active={this.props.loading} inline /> render(){ const { joke, loading } = this.props; return( <div> { (loading ? this.renderLoader() : this.renderJoke(joke) ) } </div> ) } } SingleJoke.defaultProps ={ loading : true, } const mapStateToProps = (state, ownProps) => { return { joke : state.joke.data, loading : state.joke.loading, error : state.joke.error, } } const mapDispatchToProps = dispatch => { return bindActionCreators(jokeActions, dispatch) } export default connect(mapStateToProps, mapDispatchToProps)(SingleJoke);

Our components will be rendered as children of the main App component, to allow this we’ll need to update App.js as shown below.

js
import React, { Component } from 'react'; import './sass/main.scss'; import TitleBar from './components/TitleBar'; class App extends Component { render() { return ( <div className="main-container"> <TitleBar /> {this.props.children} </div> ); } } export default App;

Last but not least, let us handle routing. For this we’ll use React Router, to install it run:

bash
yarn add react-router-dom$

Under the src directory, create a routes.jsx file and declare the routes as follows:

jsx
import React from 'react'; import { Route, Switch } from 'react-router-dom'; import App from './App'; import Categories from './components/Categories'; import NotFound from './components/NotFound'; import SingleJoke from './components/SingleJoke'; const Routes = () => ( <App> <Switch> <Route exact path="/" component={Categories} /> <Route exact path="/categories/:category" component={SingleJoke} /> </Switch> </App> ); export default Routes;

Import the declared routes into src/index.js .

js
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import { BrowserRouter } from 'react-router-dom'; import Routes from './routes'; import registerServiceWorker from './registerServiceWorker'; import { Provider } from 'react-redux'; import configureStore from './redux/store'; const store = configureStore({}); ReactDOM.render( <Provider store={store}> <BrowserRouter> <Routes /> </BrowserRouter> </Provider>, document.getElementById('root') ); registerServiceWorker();

You will also notice we have some redux code in each of the files above including store setup and configuration. The next step would be to create our redux actions, reducers and store.

Redux setup

Under the src/redux directory create three more directories, these are actions, reducers and store which will hold these pieces of our redux setup. The first bit we’ll set up is our actions. Under the actions folder create a categoryActions.js and set up our category actions.

js
export const fetchCategories = () => dispatch => fetch('https://api.chucknorris.io/jokes/categories') .then(res => { if (!res.ok) { return res.json().Promise.reject.bind(Promise); } else { return res.json(); } }) .then(categories => { return dispatch(fetchCategoriesSuccess(categories)); }) .catch(err => { console.log(err); return dispatch(fetchCategoriesFailure(err)); }); export const fetchCategoriesSuccess = categories => { return { type: 'FETCH_CATEGORIES_SUCCESS', categories, loading: false, error: false }; }; export const fetchCategoriesFailure = err => { return { type: 'FETCH_CATEGORIES_FAILURE', loading: false, error: true, err }; };
Next set up the joke actions which will handle fetching of individual jokes. These will be placed in a file called `jokeActions.js`
js
export const fetchCategories = () => dispatch => fetch('https://api.chucknorris.io/jokes/categories') .then(res => { if (!res.ok) { return res.json().Promise.reject.bind(Promise); } else { return res.json(); } }) .then(categories => { return dispatch(fetchCategoriesSuccess(categories)); }) .catch(err => { console.log(err); return dispatch(fetchCategoriesFailure(err)); }); export const fetchCategoriesSuccess = categories => { return { type: 'FETCH_CATEGORIES_SUCCESS', categories, loading: false, error: false }; }; export const fetchCategoriesFailure = err => { return { type: 'FETCH_CATEGORIES_FAILURE', loading: false, error: true, err }; };

We’ll need to set up reducers to handle updating state when our actions are dispatched. The first one will be in categoryReducer.js which will handle updating of categories.

js
const initialState = {}; export default function reducer(state = initialState, action) { switch (action.type) { case 'FETCH_CATEGORIES_SUCCESS': return Object.assign({}, state, { data: action.categories, loading: action.loading, error: action.error }); case 'FETCH_CATEGORIES_FAILURE': return Object.assign({}, state, { data: [], loading: action.loading, error: action.error }); default: return state; } }

Now let’s set up our jokes reducer in jokeReducer.js to handle joke updates to state.

js
const initialState = {}; export default function reducer(state = initialState, action) { switch (action.type) { case 'FETCH_JOKE_SUCCESS': return Object.assign({}, state, { data: action.joke, loading: action.loading, error: action.error }); case 'FETCH_JOKE_FAILURE': return Object.assign({}, state, { data: [], loading: action.loading, error: action.error }); default: return state; } }

We will then combine our reducers in and index.js file within the reducers directory.

jsx
import { combineReducers } from 'redux'; import categories from './categoryReducer'; import joke from './jokeReducer'; export default combineReducers({ categories, joke });

The last bit of our redux set up is setting up our store. We’ll be applying thunk as our middleware to allow our actions to be anonymous functions. Inside the store directory create an index.js file and configure your store applying the required middleware.

jsx
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import { composeWithDevTools } from 'redux-devtools-extension'; import reducer from '../reducers/index'; const enhancer = composeWithDevTools(applyMiddleware(thunk)); export default function configureStore(initialState) { const store = createStore(reducer, initialState, enhancer); return store; }

Styling

To apply the relevant styling to the app, paste the contents of this folder inside your sass directory. In order to get our styling to work we’ll need to compile the sass to regular css. This compiling is done by a package called node-sass install node-sass) which is installed by running:

bash
yarn add node-sass$

That’s it, now our app is ready to go live. We can now look into testing.

Testing our Application

Our first step towards testing our application would be to install React Testing library.

bash
yarn add @testing-library/react$

We need to install an extended version of jest-dom in order to allow us to extend on the functionality of expect using testing library’s API. Run this command:

bash
yarn add @testing-library/jest-dom/extend-expect$

Snapshot Testing

We are now ready to write our of tests. We’ll be writing snapshot tests first. Snapshot tests generate a file with a copy of what the component will look like in the DOM once it is rendered. Once generated, the test creates a _snapshots_ directory in the same directory as the components being tested and creates files within it that it stores the generated copies of the rendered component.

On top of snapshots, we will also have tests that search the DOM for components and check their behaviour as well. This is where extend-expect comes in. We can get started by testing our Categories component. Create a Categories.test.js file in the same directory as Categories.jsx and get to testing. Here’s what tests for this component will look like.

jsx
import React from 'react'; import { Categories } from './Categories'; import { render, cleanup, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; const props = { fetchCategories: jest.fn(), loading: false, categories: [ 'explicit', 'dev', 'movie', 'food', 'celebrity', 'science', 'sport', 'political', 'religion', 'animal', 'history', 'music', 'travel', 'career', 'money', 'fashion' ] }; afterEach(cleanup); const propsLoading = { fetchCategories: jest.fn(), loading: true }; test('renders category cards', () => { const container = render(<Categories {...props} />); expect(container).toMatchSnapshot(); }); test('renders a loader', () => { const container = render(<Categories {...propsLoading} />); expect(container).toMatchSnapshot(); }); // Test Without Snapshot test('renders the titlebar text', () => { const { getByText } = render(<Categories {...props} />); expect(getByText('Categories')).toBeInTheDocument(); expect( getByText('Chuck Norris demands you choose a category') ).toBeInTheDocument(); fireEvent.click(getByText('dev')); expect(props.fetchCategories).toHaveBeenCalled(); });

The render function imported from testing library renders into a container which is appended to document.body. Before calling it we need to supply our component with mocked props that will be used to render our component. For data we can simply provide mocked data whereas for components Jest allows us to create a mock function jest.fn() which can then be called within the rendered component.

Our first test checks if the component renders as expected once all the data is returned. It provides a mock fetchCategories function as well as mocked data to our component when it is passed into render(). It then checks if the container matches the snapshot. This snapshot is generated the first time the toMatchSnapshot() function is called. Subsequent running of the tests compares the rendered component to the previously generated snapshot, this test fails if there’s any variation in the comparison.

The second test checks if the component renders a loader when the component is in loading state. To test for this our component is supplied with a copy of what props will look like when loading. Just like the first test a snapshot is generated and will be checked against on subsequent test runs.

You may be wondering about the one possible problem with snapshots, what happens when you refactor a component? How do we account for that in our tests? Thankfully, we can update our snapshots accordingly when necessary by running the command:

bash
yarn test --updateSnapshot$

or

bash
yarn test --u-u$

Once the tests run the previous snapshots will be overwritten with new copies that contain updated copies and you will receive a message like the one below in our console.

Image loading...Snapshot testing results

Testing without snapshots

If you do not wish to test using snapshots, you can write tests using the expect API. This is why we installed @testing-library/jest-dom/extend-expect . Our third component test makes use of this. We’ve destructured a function getByText from our rendered component, this function allows us to search through the DOM for elements identifying them by the text in them. We then call a toBeInTheDocument() function that asserts that the expected text is in the DOM.

We also make use of fireEvent which allows us to fire DOM events. In our case, we search for an element containing the text dev which is one of our categories rendered on a card. We then simulate clicking on this element. We then assert that the expected function is called using the toHaveBeenCalled().

We can now test out the rest of the components in similar fashion. For the SingleJoke component, create a SingleJoke.test.js and test it just as the Categories component.

jsx
import React from 'react'; import { render, cleanup } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { SingleJoke } from './SingleJoke'; afterEach(cleanup); const props = { fetchJoke: jest.fn(), loading: false, joke: 'Chuck Norris writes code that optimizes itself.', match: { params: { category: 'dev' } } }; const propsFailed = { fetchJoke: jest.fn(), loading: true, match: { params: { category: 'dev' } } }; test('renders the expected joke', () => { const container = render(<SingleJoke {...props} />); expect(container).toMatchSnapshot(); }); test('renders joke failure ui joke is not loaded', () => { const container = render(<SingleJoke {...propsFailed} />); expect(container).toMatchSnapshot(); }); // Test Without Snapshot test('renders the next joke button', () => { const container = render(<SingleJoke {...props} />); const { getByText } = container; expect(getByText('Next')).toBeInTheDocument(); expect(props.fetchJoke).toHaveBeenCalled(); });

And for the TitleBar

jsx
import { configure } from 'enzyme'; import React from 'react'; import { TitleBar } from './TitleBar'; import { render, cleanup } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; afterEach(cleanup); test('renders a title bar with expected components', () => { const titlebar = render(<TitleBar />); expect(titlebar).toMatchSnapshot(); }) // Test Without Snapshot test('renders the titlebar text', () => { const { getByText } = render(<TitleBar />); expect(getByText('Joke Norris')).toBeInTheDocument(); });

One common pattern you may have observed across all our test file is cleanup which is called after each test is run using the afterEach function, it unmounts React trees that were mounted with render. This ensures each of our tests has a freshly rendered component avoiding memory leaks, this makes sure each test is independent making it easier to spot bugs.

Running our tests should return feedback similar to the one shown below indicating how many tests and test suites were run and how many snapshots were checked against.

Image loading...Testing without snapshots

Conclusion

React Testing Library is a fantastic testing solution that is both easy to use and light-weight. This library encourages your applications to be more accessible and allows you to get your tests closer to using your components the way a user will, which allows your tests to give you more confidence that your application will work when a real user uses it. For this reason it is ideal for most testing needs.

Read similar articles