10 min read testing

Basic ATDD Example with React

Step-by-step real-world example of ATDD (Acceptance Test Driven Development) 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.

[Top]

Read Next

Post image for Fundamentals of ATDD
Basics of ATDD and how to approach in real-life development.

Fundamentals of ATDD

Post image for Dropdowns in React: Unveiling the Compound Component Pattern
A deep dive into creating a feature-rich React dropdown menu, enhanced with keyboard navigation and the Compound Component Pattern for improved accessibility and maintainability.