Testing React Components: The Snapshot & Component Testing in Isolation
Tags: react, testing, jest, storybook
Published: 2023-02-06 17:05:53

React is a widely used JavaScript library for building user interfaces and single-page applications. To ensure the quality and reliability of your React code, testing is crucial.

There are two main forms of testing in React: snapshot testing and component testing. Snapshot testing helps detect unexpected changes in your components' output, while component testing using Storybook allows you to verify the behavior and interactions of individual components in isolation. In this article, we will dive into both methods and demonstrate how to implement them in your React projects. Whether you are an experienced React developer or just starting, this guide will provide you with the essential tools and techniques for enhancing your testing process. So, let's get started!

### Getting Started

*Note: You can refer to [this](https://github.com/Nazeeh21/react-testing) repo for the reference*

Initialize the new next app by running `npx create-next-app --ts [project name]` . Then navigate to your project folder. For this tutorial, we will develop a `Button` component and add some props to make its properties more dynamic. So, at the root of your project, create a new folder named `Components` , and in that create another folder named `Button`. This folder will contain all our files related to the `Button` component like the actual component, its test, etc. In this folder create a file named `Button.tsx`. In this file, paste the following code.

```typescript
import React from "react";

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  label: string;
  hasError?: boolean;
  bg?: string;
  color?: string;
}

const Button: React.FC<ButtonProps> = ({ label, hasError, bg, color, ...props }) => {
  return (
    <button
      {...props}
      style={{ background: bg, color, borderColor: hasError ? "red" : "green" }}
    >
      {label}
    </button>
  );
};

export default Button;
```

We have defined a `Button` component and are accepting props to control its properties from the parent component. Let's carry out end-to-end testing of this component using `storybook` and snapshot testing using `jest`.

### Snapshot testing

Snapshot testing is a helpful tool to ensure that your UI does not change unexpectedly. It helps to render UI, takes a snapshot, then compares it to a reference snapshot. if both snapshots do not match, then either something has been changed or we need to update the snapshot with the new version.

For snapshot testing, we will be using [jest]([https://jestjs.io/](https://jestjs.io/)). So let's get started with writing tests. But before that, we'll need to add some libs. So run the following command in your project root.

```bash
yarn add --dev jest @types/jest ts-jest babel-jest @babel/preset-env @babel/preset-react @babel/preset-typescript react-test-renderer @types/react-test-renderer
```

Once the above command succeeds, create a new file called `babel.config.js` in your project root and make sure that it looks like the one below.

```javascript
module.exports = {
  presets: [
    "@babel/preset-env",
    "@babel/preset-typescript",
    ["@babel/preset-react", { runtime: "automatic" }],
  ],
};
```

We've all the prerequisites, let's write our test. For that, create a new file named `Button.test.tsx` inside the `Button` folder under `Components`. In this test file, paste the code below.

```typescript
import renderer from "react-test-renderer";
import Button from "./Button";

it("renders correctly", () => {
  const tree = renderer
    .create(
      <Button label="Test Button" hasError={false} bg="yellow" color="black" />
    )
    .toJSON();
  expect(tree).toMatchSnapshot();
});
```

Let's understand the above code. `it` keyword tells the compiler that this is our test. As the first argument, we can give a short heading to our test which can describe what our test is about. While running the test, we'll get to see these labels in our terminal. Following that, `renderer.create` will create a DOM of the component supplied, then will convert it to `json` and will save its snapshot. Every time we'll run our tests `expect` will match the current snapshot with the existing saved snapshot. And if the snapshot does not match then it will give an error. In such a scenario, if the change was intentional then we will have to update the snapshot or else we will have to fix that unnecessary change.

Now before running the test, add `"test": "jest"` to the `scripts` object in your `package.json`. Let's run this test by running the command `yarn run test` in your terminal. You should see an output like below in your terminal.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/69ms3qj38j3n8dd2vt8m.png)

You are running this test for the first time, thus there is not any existing snapshot to compare. As a result, it will save this snapshot and will use this to compare it in the succeeding ones.

Now let's change the `bg` of our `Button` to any other color and run the test again. You should see your test failing as shown below.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r7q1zgh8excsxle8y4lm.png)

Now, we have made this change intentionally, so we'll have to update the snapshot with this new one. For that, we will have to run `yarn run test --updateSnapshot` in our terminal. You should see an output like below in your terminal.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t15nt60p2isoi41g3mxj.png)


Congrats! You have just introduced snapshot testing to your Component. Just like this you can test every component and make sure that everything works as expected and doesn't break anything in production. Now, let's move toward testing the Component in isolation.

### Storybook: Testing in Isolation

Storybook lets us test our components visually and one component at a time. It helps us to document components for reuse and automatically visually test your components to prevent bugs. It also provides addons that help you do things like fine-tune responsive layouts or verify accessibility.

So let's get started by adding `storybook` to our project. For that run the below command in your root directory.

```bash
npx storybook init
```

Now, let's add `Story` to our component. `Story` captures the rendered state of your component. We can have multiple stories for each component, each story will describe a particular supported state of a component.

Let's create stories for our `Button` component. For that inside the `Button` folder of `components` folder, create a file named `Button.stories.tsx`. In that file, paste the below code.

```typescript
import Button from "./Button";
import React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";

export default {
  title: "Button",
  component: Button,
} as ComponentMeta<typeof Button>;
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  label: "Primary",
  hasError: false,
  bg: "blue",
  color: "white",
};

export const Secondary = Template.bind({});
Secondary.args = {
  label: "Secondary",
  hasError: false,
  bg: "yellow",
  color: "black",
};

export const ChangeColor = () => {
  const [color, setColor] = React.useState("green");
  const clickHandler = () => {
    setColor((prevColor) => (prevColor === "green" ? "yellow" : "green"));
  };

  return (
    <Button
      label="Click to change Color"
      hasError={false}
      bg={color}
      color={"black"}
      onClick={clickHandler}
    />
  );
};
```

### Explanation

Let's understand what's going on in the above code. Firstly we have to write stories according to the [Component StoryFormat](https://storybook.js.org/docs/react/api/csf) and an ES6 module-based standard. The key ingredients are the *default export* which describes the component and the *named export* which describes the stories. The *default* export metadata controls how Storybook lists your stories and provides information used by addons. Following the `default` export, we have defined `Template` which we will be using as the boilerplate for the following stories and we can just pass more arguments to it using `Template.bind({})` and don't repeat the basic boilerplate. To change the properties or to pass an addon of a component, we can define those args using `Primary.args = { }`, `Primary` being the name of the extended component, as we've done above.

Now let's see each story of our component visually in our browser. But before that update the path for our stories in `.storybook/main.js` the file. and make sure that it looks like the one below.

```javascript
module.exports = {
  "stories": [
    "../Components/**/*.stories.mdx",
    "../Components/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions"
  ],
  "framework": "@storybook/react",
  "core": {
    "builder": "@storybook/builder-webpack5"
  }
}
```

Simultaneously, delete the `stories` folder in your project root. Now let's run `yarn storybook` in your terminal.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mge7fzjp0pgl66zroeys.png)

You should be able to see such a UI in your browser. You can see each defined story for our `Button` component under the *Button* label. Try playing along with **Controls** at the bottom such as changing the label, alternating the value of `hasError` props, etc, and see the visual change above. Under the `Change Color` option, try clicking the button, it should change the *backgroundColor* of the component as defined in the *clickHandler*.

Thus, Stories helps us to check that UI continues to look correct as you make changes. By viewing these stories without the combination/inflation of other components we can confirm that our UI component is not behaving unexpectedly and is rendering correctly.

This was the brief overview of testing components using `jest` and \`storybook\` and thereby you've successfully tested your component in isolations.