10 min read testing
Basic ATDD Example with React
Introduction
This article covers a step-by-step real-world example of ATDD with React. It is a straightforward counter that can be incremented and decremented. The counter is displayed in a text box. It will have a limitation on the negative and positive numbers.
How to configure jest for TDD
Adding Jest as a dependency
First, we need to add jest as a dependency. We can do this by running the following command:
npm install --save-dev jest
Running jest package binaries with npx
In 2017, the npm
team introduced a sibling project: npx. Whereas npm
is a package manager, npx is a package runner. Among other things, npx
lets you run binaries from local Node packages without adding them to your PATH.
We can run jest
package binaries with nnpx
. We can do this by running the following command:
npx jest
It passed any extra arguments you provide to the executable that is being run with npx
:
npx jest --version
If you have been doing JavaScript/React development without npx
, you will find that it is an essential addition to your tool belt.
Running project test script with npm
We should first add a test script to our package.json
file. We can do this by adding the following to the scripts section of the file:
"test": "jest"
A closer look at a sample package.json
file with a test script:
{
"name": "test-driven-development",
"version": "1.0.0",
"description": "A test driven development sample",
"main": "index.js",
"scripts": {
"test": "jest" // <--- test script
},
"author": "Behrouz Pooladrak",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/preset-env": "^7.15.6",
"@babel/preset-react": "^7.14.5",
"babel-jest": "^27.0.6",
"jest": "^27.0.6"
}
}
We can run the project test script with npm
. We can do this by running the following command:
npm test
React Testing Library Dependencies
You should install the following dependencies:
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/dom
Create a Basic Counter with React
Business Requirements
The counter should have the following business requirements:
- The counter should be able to increment and decrement.
- The counter should have a limitation on the negative and positive numbers.
- The counter should be displayed in a text box.
Write acceptance test for the counter component and see it fails
We need to create a file called Counter.test.jsx
. We need to add the following code to the file.
import "@testing-library/jest-dom/extend-expect";
import React from "react";
import { render, screen } from "@testing-library/react";
import Counter from "./Counter"; // not yet implemented
describe("Counter", () => {
it("should display the counter", () => {
render(<Counter />);
expect(screen.getByText("0")).toBeInTheDocument();
});
});
Write the counter component and see the test pass
Then, based on the acceptance test, we need to create a Counter.jsx
file in the project’s root directory. We need to add the following code to the file.
We should pick the fastest way to pass the test, even in a fake way.
Then we should focus on the unit tests to achieve the acceptance goal.
There is a caveat here; if you focus on writing a proper test for the acceptance test, you will end up on a dead-end process. So, writing a fake code to pass the acceptance test is better than focusing on unit tests to grasp the development process better.
import React from "react";
const Counter = () => {
return <div>0</div>;
};
export default Counter;
Write a unit test for the counter component and see if it fails
We need to create a file called Counter.test.jsx
. We need to add the following code to the file.
In this case, it is the same test, and we will try to make it pass in the next step but not in a fake way.
import "@testing-library/jest-dom/extend-expect";
import React from "react";
import { render, screen } from "@testing-library/react";
import Counter from "./Counter";
describe("Counter", () => {
it("should display the counter", () => {
render(<Counter />);
expect(screen.getByText("0")).toBeInTheDocument();
});
});
Write the counter component and see the test pass
Then, based on the unit test, we need to update the Counter.jsx
file in the project’s root directory. We need to add the following code to the file.
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
return <div>{count}</div>;
};
export default Counter;
One of the essential factors for each coding step is to ensure that only written code is enough to pass the test. If you write more code than needed, you will end up with some functionality that is not covered with tests, and you break the TDD rule as well, but in real life, if you stick to this practice, you will have less uncovered code at the end of the day.
Check the code to see if you can refactor it
We can check if we can refactor it or if there is any bad smell in the code. If so, we can improve the code by refactoring it, and now we are backed with the tests. So we can refactor the code with confidence. After any pass test step, we can refactor the code. If we can not refactor the code, we can skip this step and move to the next step.
For the sake of simplicity, I will not add this step from here, but you should always consider this step after each pass test step.
Write a unit test for the counter component and see if it fails
We need to update the Counter.test.jsx
file in the project’s root directory. We need to add the following code to the file.
import "@testing-library/jest-dom/extend-expect";
import React from "react";
import { render, screen } from "@testing-library/react";
import Counter from "./Counter";
describe("Counter", () => {
it("should display the counter", () => {
render(<Counter />);
expect(screen.getByText("0")).toBeInTheDocument();
});
it("should increment the counter", () => {
render(<Counter />);
const incrementButton = screen.getByText("+");
incrementButton.click();
expect(screen.getByText("1")).toBeInTheDocument();
});
});
Write the counter component and see the test pass
Then, based on the unit test, we need to update the Counter.jsx
file in the project’s root directory. We need to add the following code to the file.
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<div>
<button onClick={increment}>+</button>
<div>{count}</div>
</div>
);
};
export default Counter;
Write a unit test for the counter component and see if it fails
We need to update the Counter.test.jsx
file in the project’s root directory. We need to add the following code to the file.
import "@testing-library/jest-dom/extend-expect";
import React from "react";
import { render, screen } from "@testing-library/react";
import Counter from "./Counter";
describe("Counter", () => {
it("should display the counter", () => {
render(<Counter />);
expect(screen.getByText("0")).toBeInTheDocument();
});
it("should increment the counter", () => {
render(<Counter />);
const incrementButton = screen.getByText("+");
incrementButton.click();
expect(screen.getByText("1")).toBeInTheDocument();
});
it("should decrement the counter", () => {
render(<Counter />);
const decrementButton = screen.getByText("-");
decrementButton.click();
expect(screen.getByText("-1")).toBeInTheDocument();
});
});
Write the counter component and see the test pass
Then, based on the unit test, we need to update the Counter.jsx
file in the project’s root directory. We need to add the following code to the file.
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return (
<div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<div>{count}</div>
</div>
);
};
export default Counter;
Write a unit test for the counter component and see if it fails
We need to update the Counter.test.jsx
file in the project’s root directory. We need to add the following code to the file.
import "@testing-library/jest-dom/extend-expect";
import React from "react";
import { render, screen } from "@testing-library/react";
import Counter from "./Counter";
describe("Counter", () => {
it("should display the counter", () => {
render(<Counter />);
expect(screen.getByText("0")).toBeInTheDocument();
});
it("should increment the counter", () => {
render(<Counter />);
const incrementButton = screen.getByText("+");
incrementButton.click();
expect(screen.getByText("1")).toBeInTheDocument();
});
it("should decrement the counter", () => {
render(<Counter />);
const decrementButton = screen.getByText("-");
decrementButton.click();
expect(screen.getByText("-1")).toBeInTheDocument();
});
it("should not decrement the counter below zero", () => {
render(<Counter />);
const decrementButton = screen.getByText("-");
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
expect(screen.getByText("0")).toBeInTheDocument();
});
});
Write the counter component and see the test pass
Then, based on the unit test, we need to update the Counter.jsx
file in the project’s root directory. We need to add the following code to the file.
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => {
if (count > 0) {
setCount(count - 1);
}
};
return (
<div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<div>{count}</div>
</div>
);
};
export default Counter;
Write a unit test for the counter component and see if it fails
We need to create a file called Counter.test.jsx
. We need to add the following code to the file. In this case, it is the same test, and we will try to make it pass in the next step but not in a fake way.
import "@testing-library/jest-dom/extend-expect";
import React from "react";
import { render, screen } from "@testing-library/react";
import Counter from "./Counter";
describe("Counter", () => {
it("should display the counter", () => {
render(<Counter />);
expect(screen.getByText("0")).toBeInTheDocument();
});
it("should increment the counter", () => {
render(<Counter />);
const incrementButton = screen.getByText("+");
incrementButton.click();
expect(screen.getByText("1")).toBeInTheDocument();
});
// We have updated the test based on the business requirement to have a limit for negative numbers
it("should decrement the counter", () => {
render(<Counter />);
const decrementButton = screen.getByText("-");
const incrementButton = screen.getByText("+");
incrementButton.click();
incrementButton.click();
decrementButton.click();
expect(screen.getByText("1")).toBeInTheDocument();
});
it("should not decrement the counter below zero", () => {
render(<Counter />);
const decrementButton = screen.getByText("-");
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
decrementButton.click();
expect(screen.getByText("0")).toBeInTheDocument();
});
it("should not increment the counter above 10", () => {
render(<Counter />);
const incrementButton = screen.getByText("+");
incrementButton.click();
incrementButton.click();
incrementButton.click();
incrementButton.click();
incrementButton.click();
incrementButton.click();
incrementButton.click();
incrementButton.click();
incrementButton.click();
incrementButton.click();
incrementButton.click();
incrementButton.click();
expect(screen.getByText("10")).toBeInTheDocument();
});
});
Write the counter component and see the test pass
Then, based on the unit test, we need to update the Counter.jsx
file in the project’s root directory. We need to add the following code to the file.
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => {
if (count < 10) {
setCount(count + 1);
}
};
const decrement = () => {
if (count > 0) {
setCount(count - 1);
}
};
return (
<div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<div>{count}</div>
</div>
);
};
export default Counter;
The Final Refactor for the Counter Component
We need to check if we can refactor the Counter.jsx
; the difference now is adequate test coverage. We can improve it confidently, and our tests will give great information to other developers as business requirements documentation.
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(currentCount => count < 10 ? currentCount + 1: currentCount);
};
const decrement = () => {
setCount(currentCount => currentCount >0 ? currentCount - 1 : currentCount);
};
return (
<div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<div>{count}</div>
</div>
);
};
export default Counter;
Write acceptance test for the next business requirement and continue the cycle
We can continue the cycle till we cover all the business requirements. We can write the acceptance test for the following business requirement and then update the code to make the acceptance test pass. This example, for sure, is not a complete application. But it is an excellent example of how we can use the ATDD approach to develop a React application.
Conclusion
This article shows how we can use the ATDD approach to develop a React application. We have seen how we can write an acceptance test for the business requirements and then update the code to make the acceptance test pass. We have also seen how we can write a unit test for the components and then edit the code to make the unit test pass. We have also seen how we can use the ATDD approach to develop a React application.
I will come up with more complex examples in future articles. I will also develop articles on how we can use the ATDD approach to create a React application with Redux and React Router.