React Test-driven Development: From User Stories to Production
Employing a test-driven development (TDD) approach in React projects tends to be straightforward, especially with the aid of Jest and Enzyme. However, there are a few things to look out for.
In this article, a Toptal JavaScript expert demonstrates his React TDD process, from epics and user stories to development and production.
Employing a test-driven development (TDD) approach in React projects tends to be straightforward, especially with the aid of Jest and Enzyme. However, there are a few things to look out for.
In this article, a Toptal JavaScript expert demonstrates his React TDD process, from epics and user stories to development and production.
In this post, we’re going to develop a React app using test-driven development (TDD) from user stories to development. Also, we are going to use Jest and Enzyme for the TDD. Upon completion of this guide, you will be able to:
- Create epics and user stories based on the requirements.
- Create tests based on user stories.
- Develop a React app using TDD.
- Use Enzyme and Jest to test a React app.
- Use/reuse CSS variables for responsive design.
- Create a reusable React component that renders and functions differently based on the provided props.
- Type check component props using React PropTypes.
This article assumes that you have basic knowledge of React. If you’re completely new to React, I’d recommend you complete the official tutorial and take a look at Toptal’s 2019 React Tutorial: Part 1 and Part 2.
Overview of Our Test-driven React App
We’ll be building a basic pomodoro timer app consisting of some UI components. Each component will have a separate set of tests in a corresponding test file. First of all, we could create epics and user stories as follows based on our project requirements.
EPIC | USER STORY | ACCEPTANCE CRITERIA |
As a user, I need to use the timer so that I can manage my time. | As a user, I need to start the timer so that I can count down my time. | Ensure the user is able to: *start the timer *see the timer start counting down Counting down the time should not be interrupted even if the user clicks the start button more than once. |
As a user, I need to stop the timer so that I can count down my time only when needed. | Ensure the user is able to: *stop the timer *see the timer stopped Nothing should happen even if the user clicks the stop button more than once. | |
As a user, I need to reset the timer so that I can count down my time from the beginning. | Ensure the user is able to: *reset the timer *see the timer reset to the default |
Wireframe
Project Setup
First, we are going to create a React project using Create React App as follows:
$ npx create-react-app react-timer
$ cd react-timer
$ npm start
You will see a new browser tab open at the URL http://localhost:3000. You can stop the running React app using Ctrl+C.
Now, we’re going to add Jest and Enzyme and some dependencies as follows:
$ npm i -D enzyme
$ npm i -D react-test-renderer enzyme-adapter-react-16
Also, we will add or update a file called setupTests.js in the src directory:
import { configure } from ‘enzyme’;
import Adapter from ‘enzyme-adapter-react-16’;
configure({ adapter: new Adapter() });
Since Create React App runs the setupTests.js file before each test, it will execute and properly configure Enzyme.
Configuring CSS
We are going to write variables and a basic CSS reset because we want the CSS variables globally available in the application. We’ll define the variables from the :root scope. The syntax for defining variables is to use custom property notation, each beginning with – followed by the variable name.
Navigate to the index.css file and add the following:
:root {
--main-font: “Roboto”, sans-serif;
}
body, div, p {
margin: 0;
padding: 0;
}
Now, we need to import the CSS into our application. Update the index.js file as follows:
import React from ‘react’;
import ReactDOM from ‘react-dom’;
import ‘./index.css’;
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>
document.getElementById(“root”)
)
Shallow Render Test
As you might already know, the TDD process would look like this:
- Add a test.
- Run all tests, and you will see the test fails.
- Write the code to pass the test.
- Run all the tests.
- Refactor.
- Repeat.
Hence, we’re going to add the first test for a shallow render test and then write the code to pass the test. Add a new spec file named App.spec.js to src/components/App directory as follows:
import React from ‘react’;
import { shallow } from ‘enzyme’;
import App from ‘./App’;
describe(‘App’, () => {
it(‘should render a <div />’, () => {
const container = shallow(<App />);
expect(container.find(‘div’).length).toEqual(1);
});
});
Then, you can run the test:
$ npm test
You will see the test fails.
App Component
Now, we will proceed to create the App component to pass the test. Navigate to App.jsx in the directory src/components/App and add the code as follows:
import React from ‘react’;
const App = () => <div className=”app-container” />;
export default App;
Now, run the test again.
$ npm test
The first test should now pass.
Adding App CSS
We are going to create a file App.css in the directory src/components/App to add some style to the App component as follows:
.app-container {
height: 100vh;
width: 100vw;
align-items: center;
display: flex;
justify-content: center;
}
Now, we are ready to import the CSS to the App.jsx file:
import React from ‘react’;
import ‘./App.css’;
const App = () => <div className=”app-container” />;
export default App;
Next, we have to update the index.js file to import the App component as follows:
import React from "react"
import ReactDOM from "react-dom"
import "./index.css"
import App from "./components/App/App"
import * as serviceWorker from "./serviceWorker"
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()
Adding the Timer Component
Finally, the app will contain the Timer component, hence we’re going to update the App.spec.js file to check for the presence of the Timer component in our app. Also, we’re going to declare the container variable outside the first test case since the shallow render test needs to be done before each test case.
import React from "react"
import { shallow } from "enzyme"
import App from "./App"
import Timer from "../Timer/Timer"
describe("App", () => {
let container
beforeEach(() => (container = shallow(<App />)))
it("should render a <div />", () => {
expect(container.find("div").length).toEqual(1)
})
it("should render the Timer Component", () => {
expect(container.containsMatchingElement(<Timer />)).toEqual(true)
})
})
If you run npm test
at this stage, the test will fail since the Timer component does not exist yet.
Writing the Timer Shallow Rendering Test
Now, we are going to create a file named Timer.spec.js in a new directory named Timer under the src/components directory.
Also, we will add the shallow render test in the Timer.spec.js file:
import React from "react"
import { shallow } from "enzyme"
import Timer from "./Timer"
describe("Timer", () => {
let container
beforeEach(() => (container = shallow(<Timer />)))
it("should render a <div />", () => {
expect(container.find("div").length).toBeGreaterThanOrEqual(1)
})
})
The test will fail, as expected.
Creating the Timer Component
Next, let’s create a new file called Timer.jsx and define the same variables and methods based on the user stories:
import React, { Component } from 'react';
class Timer extends Component {
constructor(props) {
super(props);
this.state = {
minutes: 25,
seconds: 0,
isOn: false
};
}
startTimer() {
console.log('Starting timer.');
}
stopTimer() {
console.log('Stopping timer.');
}
resetTimer() {
console.log('Resetting timer.');
}
render = () => {
return <div className="timer-container" />;
};
}
export default Timer;
This should pass the test and should render a <div />
in the Timer.spec.js file, but the test should not render the Timer Component since we haven’t added the Timer component in the app component yet.
We’re going to add the Timer component in the App.jsx file like this:
import React from 'react';
import './App.css';
import Timer from '../Timer/Timer';
const App = () => (
<div className="app-container">
<Timer />
</div>
);
export default App;
All tests should pass now.
Adding Timer CSS
We’re going to add CSS variables related to the Timer and add media queries for smaller devices.
Update the file index.css as follows:
:root {
--timer-background-color: #FFFFFF;
--timer-border: 1px solid #000000;
--timer-height: 70%;
--timer-width: 70%;
}
body, div, p {
margin: 0;
padding: 0;
}
@media screen and (max-width: 1024px) {
:root {
--timer-height: 100%;
--timer-width: 100%;
}
}
Also, we’re going to create the Timer.css file under the directory components/Timer:
.timer-container {
background-color: var(--timer-background-color);
border: var(--timer-border);
height: var(--timer-height);
width: var(--timer-width);
}
We have to update Timer.jsx to import the Timer.css file.
import React, { Component } from "react"
import "./Timer.css"
If you run the React app now, you will see a simple screen with the border on your browser.
Write the TimerButton Shallow Rendering Test
We need three buttons: Start, Stop, and Reset, hence we are going to create the TimerButton Component.
First, we need to update the Timer.spec.js file to check for the existence of the TimerButton component in the Timer component:
it("should render instances of the TimerButton component", () => {
expect(container.find("TimerButton").length).toEqual(3)
})
Now, let’s add the TimerButton.spec.js file in a new directory called TimerButton under the src/components directory and let’s add the test to the file like this:
import React from "react"
import { shallow } from "enzyme"
import TimerButton from "./TimerButton"
describe("TimerButton", () => {
let container
beforeEach(() => {
container = shallow(
<TimerButton
buttonAction={jest.fn()}
buttonValue={""}
/>
)
})
it("should render a <div />", () => {
expect(container.find("div").length).toBeGreaterThanOrEqual(1)
})
})
Now, if you run the test, you will see the test fails.
Let’s create the TimerButton.jsx file for the TimerButton component:
import React from 'react';
import PropTypes from 'prop-types';
const TimerButton = ({ buttonAction, buttonValue }) => (
<div className="button-container" />
);
TimerButton.propTypes = {
buttonAction: PropTypes.func.isRequired,
buttonValue: PropTypes.string.isRequired,
};
export default TimerButton;
If you run npm test
at this stage, the test should render instances of the TimerButton component but will fail since we haven’t added the TimerButton components to the Timer component yet.
Let’s import the TimerButton component and add three TimerButton components in the render method in Timer.jsx:
render = () => {
return (
<div className="timer-container">
<div className="time-display"></div>
<div className="timer-button-container">
<TimerButton buttonAction={this.startTimer} buttonValue={'Start'} />
<TimerButton buttonAction={this.stopTimer} buttonValue={'Stop'} />
<TimerButton buttonAction={this.resetTimer} buttonValue={'Reset'} />
</div>
</div>
);
};
TimerButton CSS
Now, it’s time to add CSS variables for the TimerButton component. Let’s add variables in the :root scope to the index.css file:
:root {
...
--button-border: 3px solid #000000;
--button-text-size: 2em;
}
@media screen and (max-width: 1024px) {
:root {
…
--button-text-size: 4em;
}
}
Also, let’s create a file called TimerButton.css in the TimerButton directory under the src/components directory:
.button-container {
flex: 1 1 auto;
text-align: center;
margin: 0px 20px;
border: var(--button-border);
font-size: var(--button-text-size);
}
.button-container:hover {
cursor: pointer;
}
Let’s update the TimerButton.jsx accordingly to import the TimerButton.css file and to display the button value:
import React from 'react';
import PropTypes from 'prop-types';
import './TimerButton.css';
const TimerButton = ({ buttonAction, buttonValue }) => (
<div className="button-container">
<p className="button-value">{buttonValue}</p>
</div>
);
TimerButton.propTypes = {
buttonAction: PropTypes.func.isRequired,
buttonValue: PropTypes.string.isRequired,
};
export default TimerButton;
Also, we need to update the Timer.css to align the three buttons horizontally, so let’s update the Timer.css file as well:
import React from 'react';
import PropTypes from 'prop-types';
import './TimerButton.css';
const TimerButton = ({ buttonAction, buttonValue }) => (
<div className="button-container">
<p className="button-value">{buttonValue}</p>
</div>
);
TimerButton.propTypes = {
buttonAction: PropTypes.func.isRequired,
buttonValue: PropTypes.string.isRequired,
};
export default TimerButton;
If you run the React app now, you will see a screen as follows:
Refactoring the Timer
We’re going to refactor the Timer since we want to implement functions like startTimer, stopTimer, restartTimer, and resetTimer. Let’s update the Timer.spec.js file first:
describe('mounted Timer', () => {
let container;
beforeEach(() => (container = mount(<Timer />)));
it('invokes startTimer when the start button is clicked', () => {
const spy = jest.spyOn(container.instance(), 'startTimer');
container.instance().forceUpdate();
expect(spy).toHaveBeenCalledTimes(0);
container.find('.start-timer').first().simulate('click');
expect(spy).toHaveBeenCalledTimes(1);
});
it('invokes stopTimer when the stop button is clicked', () => {
const spy = jest.spyOn(container.instance(), 'stopTimer');
container.instance().forceUpdate();
expect(spy).toHaveBeenCalledTimes(0);
container.find('.stop-timer').first().simulate('click');
expect(spy).toHaveBeenCalledTimes(1);
});
it('invokes resetTimer when the reset button is clicked', () => {
const spy = jest.spyOn(container.instance(), 'resetTimer');
container.instance().forceUpdate();
expect(spy).toHaveBeenCalledTimes(0);
container.find('.reset-timer').first().simulate('click');
expect(spy).toHaveBeenCalledTimes(1);
});
});
If you run the test, you will see the added tests fail since we haven’t updated the TimerButton component yet. Let’s update the TimerButton component to add the click event:
const TimerButton = ({ buttonAction, buttonValue }) => (
<div className="button-container" onClick={() => buttonAction()}>
<p className="button-value">{buttonValue}</p>
</div>
);
Now, the tests should pass.
Next, we are going to add more tests to check the state when each function is invoked in the mounted Timer test case:
it('should change isOn state true when the start button is clicked', () => {
container.instance().forceUpdate();
container.find('.start-timer').first().simulate('click');
expect(container.instance().state.isOn).toEqual(true);
});
it('should change isOn state false when the stop button is clicked', () => {
container.instance().forceUpdate();
container.find('.stop-timer').first().simulate('click');
expect(container.instance().state.isOn).toEqual(false);
});
it('should change isOn state false when the reset button is clicked', () => {
container.instance().forceUpdate();
container.find('.stop-timer').first().simulate('click');
expect(container.instance().state.isOn).toEqual(false);
expect(container.instance().state.minutes).toEqual(25);
expect(container.instance().state.seconds).toEqual(0);
});
If you run the tests, you will see them fail since we haven’t implemented each method yet. So let’s implement each function to pass the tests:
startTimer() {
this.setState({ isOn: true });
}
stopTimer() {
this.setState({ isOn: false });
}
resetTimer() {
this.stopTimer();
this.setState({
minutes: 25,
seconds: 0,
});
}
You will see the tests pass if you run them. Now, let’s implement the remaining functions in Timer.jsx:
import React, { Component } from 'react';
import './Timer.css';
import TimerButton from '../TimerButton/TimerButton';
class Timer extends Component {
constructor(props) {
super(props);
this.state = {
minutes: 25,
seconds: 0,
isOn: false,
};
this.startTimer = this.startTimer.bind(this);
this.stopTimer = this.stopTimer.bind(this);
this.resetTimer = this.resetTimer.bind(this);
}
startTimer() {
if (this.state.isOn === true) {
return;
}
this.myInterval = setInterval(() => {
const { seconds, minutes } = this.state;
if (seconds > 0) {
this.setState(({ seconds }) => ({
seconds: seconds - 1,
}));
}
if (seconds === 0) {
if (minutes === 0) {
clearInterval(this.myInterval);
} else {
this.setState(({ minutes }) => ({
minutes: minutes - 1,
seconds: 59,
}));
}
}
}, 1000);
this.setState({ isOn: true });
}
stopTimer() {
clearInterval(this.myInterval);
this.setState({ isOn: false });
}
resetTimer() {
this.stopTimer();
this.setState({
minutes: 25,
seconds: 0,
});
}
render = () => {
const { minutes, seconds } = this.state;
return (
<div className="timer-container">
<div className="time-display">
{minutes}:{seconds < 10 ? `0${seconds}` : seconds}
</div>
<div className="timer-button-container">
<TimerButton
className="start-timer"
buttonAction={this.startTimer}
buttonValue={'Start'}
/>
<TimerButton
className="stop-timer"
buttonAction={this.stopTimer}
buttonValue={'Stop'}
/>
<TimerButton
className="reset-timer"
buttonAction={this.resetTimer}
buttonValue={'Reset'}
/>
</div>
</div>
);
};
}
export default Timer;
You will see all functions work based on the user stories we prepared earlier.
So, that’s how we have developed a basic React app using TDD. If the user stories and the acceptance criteria are more detailed, the test cases can be written more precisely, thus contributing even more.
Wrapping Up
When developing an application using TDD, it’s very important not only to break down the project to epics or user stories but also to prepare well for acceptance criteria. In this article, I wanted to show you how to break down the project and use the prepared acceptance criteria for the React TDD development.
Even though there are many resources related to React TDD out there, I hope this article helped you learn a bit about TDD development with React using user stories.
Further Reading on the Toptal Blog:
Understanding the basics
How do you do test-driven development?
For test-driven development, you must first create a test, and confirm if the test fails. Once you confirm the test fails, write code to pass the test, and refactor it if necessary. Then repeat all of these steps for the next tests.
What should I test in React?
The main reason behind writing tests is to ensure the app works as it should. In React, we should test if the app renders properly without errors. Also, we should test the output and the states, and the events. Lastly, we should test edge cases like other technology stacks.
Is test-driven development good?
Test-driven development is beneficial because it reduces production bugs and improves code quality, making code maintenance easier. It also provides automated testing for regression testing. However, TDD can be costly for GUI testing, and too much TDD makes code more complicated.