The detailed guide on how to apply the TDD methodology in react applications Testing is a very important part of our life. Testing gives you confidence about things. And now you are in the kitchen making a dressing for a salad. You mix up all of the ingredients, and you know their taste more or less. The result of your process will be a salad with souse. Before putting souse into the salad, you possibly want to check — is it salty, sweet, or spicy enough? And in the end, you spice up a salad with dressing. Trying to understand how it is before accomplishing the end of the process is testing. We test food before serving and the strength of the components before building the bridge. We test the medications before sending them to pharmacies for buyers. NASA made many tests before launching the rocket. A dozen examples could be given here, but the idea of testing is everywhere. With coding, it’s the same. Writing the code in our projects is like a snowball as it grows — the chance of getting an error and being crushed in the end is very high. Also, other developers get onto the project, and the chance of breaking something is higher if there are no tests. When something in the service goes wrong — the business spends time and money fixing it. So, it’s better to have a prediction of potential mistakes by tests. Reading “Clean Code” by Robert C. Martin, I thought it is very difficult to think about tests first and then create a component. But after some training and the techniques described below, I feel more confident with TDD than ever. Here you will learn how to use tests for React applications and testing behavior in general. You will see the difference between types of tests, where to use them, and when. Be ready for a lot of coding. Before starting to write tests, I’d like to highlight three important concepts: — Domain-Driven Development DDD — Behaviour-Driven Development BDD — Test-Driven Development TDD What Is DD? DD means Driven Development. This means development is based on some behavior. Take a look at the diagram below: DDD This is the correct interpretation of a business idea in code. It’s like the skill of writing enterprise code; the code produced is based on the business idea. It includes such crucial aspects as ubiquitous language, which is a unique communication language between the business and the development sides. On the other hand, Model-Driven Design is based on a model and some patterns for development. BDD This means improving communication between the development team and the business. This concept branched from TDD; it’s like a variant or extension of it. BDD is a description of the behavior, and it’s good for integration testing and e2e. TDD It’s a development based on testing. TDD believes that you should write tests before writing the code, so you can see if it will fail. And only after the written test do you start writing the functionality for the written test and figuring out the components to satisfy the tests. It’s very good for unit testing. Tests — it’s a type of testing when you test one element (or entity), without any binding (wiring) with other elements. Unit testing — it’s about rendering logic when React rendered something to the DOM. It looks at interrelated functions, enumeration, some logic dependency functions, and so on. Integration tests — when we test how users interact with interfaces. It focuses on the user's behavior with the interface at the end of rendering. There is the browser and created user who walks through an interface. E2E testing Application On the schema, you can see that the application is very simple. In the end, you can find the link to the . But for now, let’s focus on building the application following the TDD methodology. We’re not gonna implement all of the components. It’s important to understand only how to use TDD in React. GitHub repository In the beginning, it is very important to understand what we’re gonna build and determine what components we need to implement this application. In my explanation, we’re gonna start with the simple ones. The idea of the application is to render the grid of cards requested from a fake API. Also, we have to be able to add a new card by clicking on the button, and the button has to remove all of the cards from the grid. And finally, in the , we show the total number of cards. Each card we have to be able to be opened or removed. I was using . Add Clear Footer create-react-app "dependencies": { "axios": "^0.27.2", "jest-styled-components": "^7.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router": "^6.4.0", "react-router-dom": "^6.4.0", "react-scripts": "5.0.1", "styled-components": "^5.3.5", "web-vitals": "^2.1.4" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^13.5.0", "babel-eslint": "^10.1.0", "eslint": "^8.23.0", "prettier": "^2.7.1", "react-test-renderer": "^18.2.0" } After installing packages, let’s struct our folders. Each component and view folder has to have as a file. index.js └── src ├── App ├── Routes ├── __mocks__ ├── components │ ├── Card │ ├── Cards │ ├── Footer │ ├── Header │ └── MainLayout ├── context ├── hooks ├── index.js └── views ├── AboutView ├── CardView ├── CardsView └── HomeView Routing in the application has to have the following routes: / -> Home Page /cards -> Cards Grid Page /card/:id -> Card View /about -> About View /* -> Not Found View Tests While we described the application in technical language, we already called some components. Each component has to be independent of tests. We have to be able to test components without side dependencies. We will start writing our components from views. We need them to render the itself and test routing. App └── views ├── AboutView ├── CardView ├── CardsView └── HomeView In the folder, , let’s put in each file. For example, and then open it. src/views/ index.js. <VIEW_NAME>.spec.js AboutView.spec.js describe("<AboutView />", () => { it("should render view", () => { throw Error("Not implemented"); }); }); Yes, we’re throwing the and you have to get used to it because the most important part is the description. We have to describe what we gonna test in each and what we expect from each test for that component. Now add the following to : Error('Not implemented') it index.js export const AboutView = () => <div /> And back to your test file. We gonna write the test exactly to our expectations from this component. What are we expecting? After rendering, we’re gonna save a snapshot and title. About View import { render, screen } from "@testing-library/react"; import { AboutView } from "./index"; describe("<AboutView />", () => { it("should render view", () => { render(<AboutView />); expect(screen.getByTestId("about-view")).toMatchSnapshot(); expect(screen.getByTestId("about-view")).toHaveTextContent("About View"); }); }); Jest helps us save a snapshot of our structure of components to disk and, on subsequent test runs, compare new snapshots with previously saved ones. A snapshot, in this case, is just a textual representation of the data structure. The first time a test snapshot fires, it will write the result of the component’s textual representation to disk. The test will pass and be recorded as a snapshot. The next time it manipulates the component, the test will fail because there will be a difference in the data written to the snapshot. Why it’s important? To compare and prevent unwanted elements in the component. It is very important to check the difference in snapshots. The snapshot test will be crushed by default if an undefined value is inside. Back to and adapt our view component exactly for the test. index.js export const AboutView = () => { return ( <div data-testid="about-view"> <h1>About View</h1> </div> ); }; Do the same for all other views. Write a test and then implementation for your component, function, util, hook, etc. This is the main concept of TDD. Routes As you know, we have in our app that will render our views. In the folder, , create and and then add this to the file: Routes src/Routes index.js Routes.spec.js describe("<Routes />", () => { it("should show home view", () => { throw Error("Not implemented"); }); it("should show cards page", () => { throw Error("Not implemented"); }); it("should show card view", () => { throw Error("Not implemented"); }); it("should show about view", () => { throw Error("Not implemented"); }); it("should show not found view", () => { throw Error("Not implemented"); }); }); By , we described exactly what we expected from the . Let’s add to first in our expectations. It’s important to understand what we are expecting from this component. it Routes HomeView import "@testing-library/jest-dom"; import { render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { Routes } from "./index"; jest.mock("src/views/HomeView", () => ({ HomeView: () => <div data-testid="home-view">HomeView</div>, })); const renderRoutes = (route = "/") => { return render( <MemoryRouter initialEntries={[route]}> <Routes /> </MemoryRouter> ); }; describe("<Routes />", () => { it("should show home view", () => { renderRoutes(); expect(screen.getByTestId("home-view")).toBeInTheDocument(); }); it("should show cards page", () => { throw new Error("Not implemented"); }); // ... }); The first test tells us that has to be by route . As you can see, we mocked it just to avoid unexpected imports from view components. We isolated by our test implementation. Let’s adapt to that test. No more, no less. HomeView / Routes import { Routes as ReactRoutes, Route, Outlet } from "react-router-dom"; import { HomeView } from "src/views/HomeView"; export const Routes = () => { return ( <ReactRoutes> <Route path="/" element={<Outlet />}> <Route index element={<HomeView />} /> </Route> </ReactRoutes> ); }; Run your test. The result has to be with fallen tests except only the first one related to . HomeView One by one, adds tests for other views. it jest.mock("src/views/HomeView", () => ({ HomeView: () => <div data-testid="home-view">HomeView</div>, })); jest.mock("src/views/CardsView", () => ({ CardsView: () => <div data-testid="cards-view">CardsView</div>, })); jest.mock("src/views/CardView", () => ({ CardView: () => <div data-testid="card-view">CardView</div>, })); jest.mock("src/views/AboutView", () => ({ AboutView: () => <div data-testid="about-view">AboutView</div>, })); describe("<Routes />", () => { it("should show home view", () => { renderRoutes(); expect(screen.getByTestId("home-view")).toBeInTheDocument(); }); it("should show cards page", () => { renderRoutes("/cards"); expect(screen.getByTestId("cards-view")).toBeInTheDocument(); }); it("should show card view", () => { renderRoutes("/cards/9"); expect(screen.getByTestId("card-view")).toBeInTheDocument(); }); it("should show about view", () => { renderRoutes("/about"); expect(screen.getByTestId("about-view")).toBeInTheDocument(); }); it("should show not found view", () => { renderRoutes("/someroute"); expect(screen.getByText("404 - Not Found")).toBeInTheDocument(); }); }); And adapt the component for each described test. import { Routes as ReactRoutes, Route, Outlet } from "react-router-dom"; import { HomeView } from "src/views/HomeView"; import { CardsView } from "src/views/CardsView"; import { CardView } from "src/views/CardView"; import { AboutView } from "src/views/AboutView"; export const Routes = () => { return ( <ReactRoutes> <Route path="/" element={<Outlet />}> <Route index element={<HomeView />} /> <Route path="/cards" element={<Outlet />}> <Route index element={<CardsView />} /> <Route path=":id" element={<CardView />} /> </Route> <Route path="/about" element={<AboutView />} /> </Route> <Route path="*" element={<h1>404 - Not Found</h1>} /> </ReactRoutes> ); }; App Component Time to bind it with the main component. Go to the folder and create files: . Remember that has to have . Let’s test it. App App index.js, App.spec.js App Routes Open , and describe the app test as we did before with . App.spec.js throw Error("Not implemented"); import { render, screen } from "@testing-library/react"; import { App } from "./index"; jest.mock("src/Routes", () => { return { Routes: () => <div data-testid="routes" />, }; }); describe("<App />", () => { it("should render app", () => { render(<App />); expect(screen.getByTestId("app")).toMatchSnapshot(); }); }); Of course, we don’t have any component yet, but we already understand that in the component, we’re gonna have . Let’s create the component. I am using . App Routes App styled-components import React from "react"; import { Routes } from "src/Routes"; import { StyledApp } from "./style"; export function App() { return ( <StyledApp data-testid="app"> <Routes /> </StyledApp> ); } Now, let’s run our test for . You will see that the snapshot will be created and the test passed. App Header Time to have some components for our . has and the two buttons and . Let’s start with tests. As usual, with . Only the first test will be ready for our component. MainLayout Header title Add Clear Test not implemented import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; import { Header } from "./index"; import { MemoryRouter } from "react-router-dom"; const header = ( <MemoryRouter> <Header /> </MemoryRouter> ); describe("<Header />", () => { it("should render Header", () => { render(header); expect(screen.getByTestId("header")).toMatchSnapshot(); }); it("should have title", () => { const { container } = render(header); expect(container).toHaveTextContent("Logo Title"); }); it("should render controls in correct view", () => { throw new Error("Test not implemented"); }); it("should call onReset function", () => { throw new Error("Test not implemented"); }); it("should call onAdd function", () => { throw new Error("Test not implemented"); }); it("should not have buttons", () => { throw new Error("Test not implemented"); }); }); And then let’s create our exact for the written test. Only satisfy the test; nothing else. Header import { StyledHeader } from "./style"; export const Header = ({ onReset, onAdd }) => { return ( <StyledHeader data-testid="header"> <h1>Logo Title</h1> </StyledHeader> ); }; Then run tests. Two first test has to be passed. OK, let’s add additionally other tests to one by one and improve for each test component. Header it("should render controls in correct view", () => { render( <MemoryRouter initialEntries={["/cards"]}> <Header /> </MemoryRouter> ); const resetButton = screen.getByRole("button", { name: "Reset" }); const addButton = screen.getByRole("button", { name: "Add" }); expect(resetButton).toBeInTheDocument(); expect(addButton).toBeInTheDocument(); }); And after adding buttons to the component — update your snapshot. Header <StyledHeader data-testid="header"> <h1>Logo Title</h1> <div className="buttons"> <button onClick={onAdd}>Add</button> <button onClick={onReset}>Reset</button> </div> </StyledHeader> As you can see, we are going step by step. The test is first, and then we do the implementation to satisfy that test. Let’s add two additional tests for the , as shown below: Header it("should render controls in correct view", () => { render( <MemoryRouter initialEntries={["/cards"]}> <Header /> </MemoryRouter> ); const resetButton = screen.getByRole("button", { name: "Reset" }); const addButton = screen.getByRole("button", { name: "Add" }); const buttons = screen.getByTestId("header-buttons"); expect(resetButton).toBeInTheDocument(); expect(addButton).toBeInTheDocument(); expect(buttons).toBeInTheDocument(); }); it("should call onReset function", () => { const onReset = jest.fn(); render( <MemoryRouter initialEntries={["/cards"]}> <Header onReset={onReset} /> </MemoryRouter> ); const element = screen.getByRole("button", { name: "Reset" }); userEvent.click(element); expect(onReset).toHaveBeenCalled(); }); it("should call onAdd function", () => { const onAdd = jest.fn(); render( <MemoryRouter initialEntries={["/cards"]}> <Header onAdd={onAdd} /> </MemoryRouter> ); const element = screen.getByRole("button", { name: "Add" }); userEvent.click(element); expect(onAdd).toHaveBeenCalled(); }); it("should not have buttons", () => { render( <MemoryRouter initialEntries={["/"]}> <Header /> </MemoryRouter> ); const element = screen.queryByTestId("header-buttons"); expect(element).not.toBeInTheDocument(); }); And, of course, adapt the component for those tests. We have to be sure that our functions have been called. import { StyledHeader, StyledButtons } from "./style"; import { Link, useLocation } from "react-router-dom"; export const Header = ({ onReset, onAdd }) => { const location = useLocation(); const isCardsView = location.pathname === "/cards"; return ( <StyledHeader data-testid="header"> <Link to="/"> <h1>Logo Title</h1> </Link> {isCardsView && ( <StyledButtons data-testid="header-buttons"> <button onClick={onAdd}>Add</button> <button onClick={onReset}>Reset</button> </StyledButtons> )} </StyledHeader> ); }; Footer This component is much simpler, but we gonna render some cards in it. You can see what it looks like above. First, create a test called . Footer.spec.js import { render, screen } from "@testing-library/react"; import { Footer } from "./index"; describe("<Footer />", () => { it("should render footer", () => { render(<Footer totalCards={9} />); expect(screen.getByTestId("footer")).toMatchSnapshot(); }); it("should have description and amount of cards", () => { render(<Footer totalCards={9} />); const footerData = screen.getByTestId("footer"); expect(footerData).toHaveTextContent("footer description"); expect(footerData).toHaveTextContent("Total Cards: 9"); }); it("should render 0 amount of cards", () => { render(<Footer />); const footerData = screen.getByTestId("footer"); expect(footerData).toHaveTextContent("Total Cards: 0"); }); }); And adapt for each case of testing the component. Almost the same as in the header but much simpler. import { StyledFooter } from "./style"; export const Footer = ({ totalCards }) => { return ( <StyledFooter data-testid="footer"> <p>footer description</p> <p>Total Cards: {totalCards || 0}</p> </StyledFooter> ); }; Hooks Yes, we have to test hooks as well in the same approach. Let’s assume we will have some queries to request cards. We have a link for that request, and the expected result in data has to be an array with and . id title For HTTP requests, we’re gonna use . First of all, we have to mock all of this. Let’s go to and create a folder . There is going to be and , and in the beginning, let’s add these mocks there: axios src/hooks useApi index.js useApi.spec.js import { renderHook } from "@testing-library/react-hooks"; import { useApi } from "./index"; import axios from "axios"; jest.mock("axios"); axios.get = jest.fn(() => Promise.resolve({ data: [] })); We can describe what we expect from our hook. describe("useApi", () => { it("should return loading", () => { const { result } = renderHook(() => useApi("/testf")); expect(result.current.loading).toBe(true); expect(result.current.data).toBe(null); expect(result.current.error).toBe(null); }); it("should return data", async () => { throw new Error("Not implemented"); }); it("should return error", async () => { throw new Error("Not implemented"); }); }); This part is already familiar, and we must adapt our hook for that test. import axios from "axios"; export const useApi = (url) => { return { data: null, loading: true, error: null }; }; Let’s describe in the test the expectation of behavior for the hook. In this case, helps to return the promised fake data in each test independently. mockImplementation it("should return data", async () => { const data = [1, 2, 3]; axios.get.mockImplementation(() => Promise.resolve({ data })); const { result, waitForValueToChange } = renderHook(() => useApi("/test")); await waitForValueToChange(() => result.current.data); expect(result.current.error).toBe(null); expect(result.current.loading).toBe(false); expect(result.current.data).toStrictEqual(data); }); it("should return error", async () => { const error = new Error("test error message"); axios.get.mockImplementation(() => Promise.reject(error)); const { result, waitForValueToChange } = renderHook(() => useApi("/test")); await waitForValueToChange(() => result.current.error); expect(result.current.error).toBe(error); }); We are waiting for changes in a specific field in each of those tests. Why do we have it? Because the hook will be used in a component with a life circle. import { useCallback, useEffect, useState } from "react"; import axios from "axios"; export const useApi = (url) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const fetchData = useCallback(async () => { setLoading(true); setError(null); try { const response = await axios.get(url); setData(response.data.slice(0, 50)); } catch (error) { setError(error); } finally { setLoading(false); } }, [url]); useEffect(() => { if (url) { fetchData(); } }, [url]); return { data, loading, error }; }; Reducer Absolutely the same approach could be for reducer in context. Let’s imagine that our application has a context, and we’re gonna use some actions right from there with the help of a reducer. Remember that we use reducer with within hook: initState useReducer export const initialState = { cards: [] }; const [state, dispatch] = useReducer(reducer, initialState); So, let’s describe our expectations from that reducer. Here’s the code: import { reducer, initialState } from "./index"; describe("reducer", () => { it("should return the initial state", () => { expect(reducer(undefined, {})).toEqual(initialState); }); it("should handle ADD_CARD", () => { expect( reducer(initialState, { type: "ADD_CARD", }) ).toEqual({ cards: [{ id: 1, title: "Title Card 1" }], }); }); it("should handle SET_CARDS", () => { throw new Error("not implemented"); }); it("should handle REMOVE_CARD", () => { throw new Error("not implemented"); }); it("should handle RESET", () => { throw new Error("not implemented"); }); }); And, of course, adapt our reducer for that tests one by one. export function reducer(state, action) { switch (action.type) { case "ADD_CARD": return { cards: [ ...state.cards, { ...action.payload, title: `Title Card ${state.cards.length + 1}`, id: state.cards.length + 1, }, ], }; case "RESET": return initialState; default: return state || initialState; } } Now, update the test for an additional description of reducer behavior. it("should handle SET_CARDS", () => { const cards = [ { id: 1, title: "Test One" }, { id: 2, title: "Test Two" }, ]; expect( reducer(initialState, { type: "SET_CARDS", payload: { cards, }, }) ).toEqual({ cards, }); }); it("should handle REMOVE_CARD", () => { expect( reducer( { cards: [{ id: 1, title: "test" }], }, { type: "REMOVE_CARD", payload: { id: 1 }, } ) ).toEqual({ cards: [], }); }); it("should handle RESET", () => { expect( reducer( { cards: [{ id: 1, title: "test" }], }, { type: "RESET", } ) ).toEqual({ cards: [], }); }); And again, adapt reducer for all written tests. case "SET_CARDS": return { cards: action.payload.cards.map((card) => ({ id: card.id, title: card.title, })), }; case "REMOVE_CARD": return { cards: state.cards.filter((card) => card.id !== action.payload.id), }; Other Components Now, try to check other components from the repository and write your own tests. There is , , with and . First, create tests and then component implementation. Card Cards MainLayout Header Footer Conclusion I just want to show you how to use TDD for your applications. In the beginning, it feels annoying, but you save a lot of time in the end. As a result, you’ll receive the fully tested independent component, which will be clear for other developers if they need to do some updates there. And in general, TDD provides stability, solidity, and proven solutions that have to be a cornerstone for any enterprise application**.** Resources GitHub repository: https://github.com/antonkalik/tdd-react-example It's always a pleasure to receive any suggestions and comments related to the topic. Feel free to ask any questions. Thank you! Also published . here