Post

Testing a normal React App

Introduction

Bootstrap a React app with Create React App by running the following command:

1
npx create-react-app GIVEN-PROJECT-NAME

Introduction

  • What is Testing and why
  • What to test and how to test
  • Testing React Components

What is Testing and why

  • When writing code, we test manually by running the code and checking the output. This is called manual testing.
  • Manual testing is time consuming and error prone.
  • Automated testing is the process of writing code to test the code.
  • It is faster and more reliable than manual testing.
  • Automated testing tests individual units of code and also the entire application.

Categories of Automated Testing

  • Unit Testing
    • Tests individual units of code in isolation
    • For example, a function or a class
    • It is the smallest type of testing and the most common
  • Integration Testing
    • Tests how different units of code work together
    • For example, testing how a function works with a database
    • It is more complex than unit testing
  • End-to-End Testing(E2E Testing)
    • Tests the entire application from start to finish
    • For example, testing how a user interacts with the application like login, sign up, etc.
    • It is the most complex type of testing

What to test and how to test

What to test

  • Test the diferent building blocks of the application
  • small units of code focused on a single task

How to test

  • Test success and failure cases
  • Rare edge cases

Testing React Components

For testing,

  • we need a tool for running our tests and asserting the results like Jest.
  • And a tool for simulating the browser environment React like testing-library/react.

Jest

  • Jest is a JavaScript testing framework maintained by Facebook.
  • It is used to test JavaScript code including React applications.
  • It is fast, easy to use and has a lot of features.

Testing a component

  • We will test a simple component called Button which takes a label prop and renders a button with the label.
1
2
3
4
5
6
7
8
// Button.js
import React from "react";

const Button = ({ label }) => {
  return <button>{label}</button>;
};

export default Button;
  • We will write a test for the Button component using Jest and testing-library/react.
1
2
3
4
5
6
7
8
9
10
// Button.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Button from "./Button";

test("renders a button with the label", () => {
  render(<Button label="Click me" />);
  const button = screen.getByText("Click me");
  expect(button).toBeInTheDocument();
});
  • Letโ€™s break down the test.

    • We need to name the test file starting with the name of the component followed by .test.js like Button.test.js.
    • We import render and screen from @testing-library/react to render the component and query the DOM.
    • We import the Button component we want to test.
    • We write a test using the test function provided by Jest.
      • The first argument is the description of the test.
      • The second argument is the test function.
  • In the test function,

    • We render the Button component with the label prop set to "Click me".
    • We query the DOM for the button with the label "Click me" using screen.getByText.
    • We assert that the button is in the document using expect(button).toBeInTheDocument().
    • If the button is not in the document, the test will fail.
    • If the button is in the document, the test will pass.
  • To run the test, we use the test script in package.json which runs jest.
  • In our example, we include the test script in package.json like this.
1
2
3
4
5
{
  "scripts": {
    "test": "jest"
  }
}
  • We run the test using the command npm test in the terminal.

  • Jest will run the test and show the result in the terminal.
  • If the test passes, Jest will show a green checkmark.
  • If the test fails, Jest will show a red cross.
  • Jest will also show the number of tests passed and the number of tests failed.

  • To simulate a failure, we can change the label in the Button component to "Click us!".
  • When we run the test, Jest will show a red cross and the error message.
  • Additionally,

    • Jest will show the difference between the expected and received values.
    • The line and position of the error in the test file.
  • We can quit test mode by pressing ctrl + c in the terminal.

Testing Suites

  • We can group related tests into a suite using the describe function provided by Jest.
    • the first argument is the description of the suite.
    • the second argument is a function containing the tests.
  • A suite can contain multiple tests and other suites.
1
2
3
4
5
6
7
8
9
10
11
12
// Button.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Button from "./Button";

describe("Button", () => {
  test("renders a button with the label", () => {
    render(<Button label="Click me" />);
    const button = screen.getByText("Click me");
    expect(button).toBeInTheDocument();
  });
});
  • In the above example, we group the test for the Button component into a suite called Button.
  • Now when we run the test, Jest will show the suite name and the test name in the terminal.

Testing User Interaction

  • We are now going to test a component called Counter which has a button to increment the count.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Counter.js
import React, { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);

  const handleChangeCounter = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleChangeCounter}>Increment</button>
    </div>
  );
};

export default Counter;
  • We will write a test for all possible user interactions with the Counter component:
    • Renders the count and the button
    • Increments the count when the button is clicked
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Counter.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Counter from "./Counter";

describe("Counter", () => {
  test("renders a button with the label", () => {
    render(<Counter />);
    const count = screen.getByText("Count: 0");
    const button = screen.getByText("Increment");
    expect(count).toBeInTheDocument();
    expect(button).toBeInTheDocument();
  });

  test("increments the count when the button is clicked", () => {
    render(<Counter />);
    const count = screen.getByText("Count: 0");
    const button = screen.getByRole("button", { name: "Increment" });
    userEvent.click(button);

    expect(count).toHaveTextContent("Count: 1");
  });
});

Testing connected components

  • Letโ€™s say our app.jsx file looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useState } from "react";

const Greeting = () => {
  const [changedText, setChangedText] = useState(false);

  const handleTextChange = () => {
    setChangedText(true);
  };

  return (
    <div>
      <h1>Hello, World!</h1>

      {changedText ? <p>Changed!</p> : <p>Welcome to my first Test app.</p>}
      <button onClick={handleTextChange}>Change Text</button>
    </div>
  );
};
export default Greeting;
  • A greeting component that has a button to change the text.
  • Our test file for the Greeting component looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Greeting from "./Greeting";

describe("Greeting component", () => {
  test("renders hello world as a text", () => {
    // Arrange
    render(<Greeting />);
    // Act
    // ... nothing
    // Assert
    const helloWorldElement = screen.getByText("Hello, World!");
    expect(helloWorldElement).toBeInTheDocument();
  });

  test("renders Welcome to my first Test app if the button was NOT clicked", () => {
    render(<Greeting />);
    const paragraphElement = screen.getByText("Welcome to my first Test app.", {
      exact: false,
    });
    expect(paragraphElement).toBeInTheDocument();
  });

  test("renders Changed! if the button was clicked", () => {
    render(<Greeting />);
    // Act
    const buttonElement = screen.getByRole("button");
    userEvent.click(buttonElement);
    // Assert
    const outputElement = screen.getByText("Changed!");
    expect(outputElement).toBeInTheDocument();
  });

  test("does not render Welcome to my first Test app if the button was clicked", () => {
    render(<Greeting />);
    // Act
    const buttonElement = screen.getByRole("button");
    userEvent.click(buttonElement);
    // Assert
    const outputElement = screen.queryByText("Welcome to my first Test app.", {
      exact: false,
    });
    expect(outputElement).toBeNull();
  });
});
  • Now letโ€™s create another component that:
    • accepts a prop
    • renders the children of the prop
1
2
3
4
5
6
7
import React from "react";

const Text = (props) => {
  return <p>{props.children}</p>;
};

export default Text;
  • We will import the Text component into the Greeting component and pass the text as a prop.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useState } from "react";
import Text from "./Text";

const Greeting = () => {
  const [changedText, setChangedText] = useState(false);

  const handleTextChange = () => {
    setChangedText(true);
  };

  return (
    <div>
      <h1>Hello, World!</h1>

      {changedText ? (
        <Text>Changed!</Text>
      ) : (
        <Text>Welcome to my first Test app.</Text>
      )}
      <button onClick={handleTextChange}>Change Text</button>
    </div>
  );
};
  • With such a simple component, we MUST not write a test for the Text component.
  • Howerver, if the Text component was a complex component with its own state for example, then we would have to write a test for it.

Testing Asynchronous Code

  • Now letโ€™s say we have a component that fetches data from an API and renders it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useEffect, useState } from "react";

const Async = () => {
  const [posts, setPosts] = useState([]);

  const fetchPosts = async () => {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    const data = await response.json();
    setPosts(data);
  };

  useEffect(() => {
    fetchPosts();
  }, []);

  return (
    <div>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default Async;
  • We will write a test for the Async component using Jest and testing-library/react.
1
2
3
4
5
6
7
8
9
10
import { render, screen } from "@testing-library/react";
import Async from "./Async";

describe("Async component", () => {
  test("renders a list of posts", async () => {
    render(<Async />);
    const listElement = await screen.findAllByRole("listitem");
    expect(listElement).not.toHaveLength(0);
  });
});
  • In the test function,

    • We render the Async component.
    • We query the DOM for the list items using screen.findAllByRole.
    • We assert that the list items are not empty using expect(listElement).not.toHaveLength(0).
    • We use await to wait for the list items to be rendered.
  • The above implementation is not ideal because it makes a real API call.
    • It is expensive for our resources.
    • It is also unreliable because the API might be down or slow.
  • It is therefore better to mock the API call.

Mocking

  • Mocking is the process of replacing a function with a fake function.
  • We use mocking to isolate the code we are testing from the rest of the application.
  • We can mock functions, modules, and objects.
  • We can also mock the return value of a function.

  • Letโ€™s mock the test for the Async component to isolate the component from the API.
    • We will use the window.fetch function to fetch the data from the API.
    • The window.fetch function will use the jest.fn function to create a mock function.
    • We will use mockResolvedValueOnce to mock the return value of the window.fetch function.
    • then we will use mockImplementation to mock the response.json function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { render, screen } from "@testing-library/react";
import Async from "./Async";

describe("Async component", () => {
  test("renders a list of posts", async () => {
    window.fetch = jest.fn();
    window.fetch.mockResolvedValueOnce({
      json: async () => [
        { id: 1, title: "Post 1" },
        { id: 2, title: "Post 2" },
      ],
    });

    render(<Async />);
    const listElement = await screen.findAllByRole("listitem");
    expect(listElement).not.toHaveLength(0);
  });
});
This post is licensed under CC BY 4.0 by the author.