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:
bashnpm install -g create-react-app
$$
Now let’s initiate our application by running:
bashcreate-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:
bashnpm 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:
bashyarn add semantic-ui-react
$$
Our state management tool will be [redux](https://redux.js.org/, to install it and it’s dependencies run:
bashyarn 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:
jsximport 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:
jsximport 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:
jsximport 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.
jsimport 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:
bashyarn add react-router-dom
$$
Under the src
directory, create a routes.jsx
file and declare the routes as follows:
jsximport 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
.
jsimport 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.
jsexport 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 }; };
defaultNext set up the joke actions which will handle fetching of individual jokes. These will be placed in a file called `jokeActions.js`
jsexport 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.
jsconst 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.
jsconst 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.
jsximport { 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.
jsximport { 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:
bashyarn 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.
bashyarn 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:
bashyarn 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.
jsximport 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:
bashyarn test --updateSnapshot
$$
or
bashyarn 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...
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.
jsximport 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
jsximport { 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...
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.