React_13

React_13

In this blog, we will learn how to test our app using Jest and React Testing Library.

Why do we need test cases?

Generally, when we build a large scale application, there a many developers who are building the application. Hence, there are many components which are integrated with each other. We write test cases to ensure that our code is intact. Suppose we add new code, it should not break existing code. If we add a new component, it should not break any of the rest of the components. Thus, test cases make our code maintainable.

What are different types for testing?

  • Manual Testing: humans required, there is a chance of error.

  • Automated Testing: Selenium

  • End-to-end Testing: covers entire user journey. QA teams use headless browser.

  • Unit Testing

  • Integration Testing

What is Enzyme?

Enzyme is a JavaScript testing utility that complements Jest to make testing components easier and more intuitive.

Here's how Enzyme fits into the picture:

  • Jest is a testing framework that provides the overall structure for running tests, assertions, and mocks.

  • Enzyme acts as a wrapper around the React DOM and makes it easier to manipulate, traverse, and assert on the rendered output of your React components.

Think of Enzyme as a magnifying glass for your components during testing. It allows you to:

  • Shallow render components: Focus on testing the component itself without its children, ideal for isolating logic and behavior.

  • Mount components: Test the entire component and its children, including connected APIs and lifecycle methods.

  • Find specific elements: Target specific DOM nodes within the rendered output for assertions.

  • Simulate user interactions: Trigger events like clicks, changes, and key presses on elements to test component responses.

  • Use matchers for assertions: Verify various aspects of the rendered output, like element existence, class presence, and content.

Benefits of using Enzyme:

  • Simplified DOM manipulation: Enzyme's API mimics jQuery for familiar operations like finding elements, changing class names, and triggering events.

  • Improved test readability: Enzyme makes test code more concise and clear, focusing on component behavior rather than DOM details.

  • Better isolation: Shallow rendering simplifies testing individual components without dependencies, leading to more maintainable tests.

  • Enhanced mocking: Enzyme allows you to easily mock child components and external APIs for focused testing.

When to use Enzyme:

  • Enzyme is most valuable for testing React components at the unit level, focusing on their internal logic and behavior.

  • It's less suitable for integration or end-to-end testing, where you need to test interactions with external systems or the entire user interface.

In summary, Enzyme is a powerful tool for unit testing React components with Jest. It makes DOM manipulation and assertions intuitive, leading to cleaner, more focused, and maintainable tests.

Enzyme vs React Testing Library

FeatureEnzymeReact Testing Library
ApproachFocuses on component implementation detailsFocuses on user experience and rendered output
RenderingOffers shallow, full rendering, and staticUses full rendering only
DOM AccessDirect access to component internalsEncourages querying by accessibility roles, labels, text content
Event SimulationProvides specific methods for triggering eventsUses fireEvent for user-like interactions
State/PropsAllows direct manipulation of state and propsEncourages testing based on rendered output, not internal state
AssertionsUses Jest's built-in assertionsOften paired with jest-dom for custom matchers
Maintained byAirbnbKent C. Dodds

When to Choose Enzyme:

  • Testing complex logic within components

  • Needing fine-grained control over rendering and state

  • Working with legacy class-based components

  • Requiring specific performance optimisations

When to Choose React Testing Library:

  • Prioritising user experience and accessibility

  • Aligning tests with how users interact with components

  • Encouraging more robust and maintainable tests

  • Working primarily with functional components

  • Following modern React best practices

Additional Considerations:

  • React Testing Library aligns better with React's philosophy of testing output, not implementation.

  • Enzyme might require more refactoring if component structure changes.

  • React Testing Library encourages more resilient tests that focus on user value.

Recommendations:

  • New projects: Start with React Testing Library for its modern approach and alignment with React best practices.

  • Existing projects: Consider migrating to React Testing Library if feasible, but Enzyme can still be effective if used judiciously.

  • Specific needs: Evaluate based on your project's specific requirements and testing goals.

Ultimately, the best choice depends on your project's specific needs and testing philosophy. Consider the trade-offs and choose the library that best suits your team's approach and the types of components you're testing.

What is Jest and why do we use it?

Jest is a JavaScript testing framework designed with a focus on simplicity and delightfulness. It's become a popular choice for testing React applications, but it's also versatile enough to work with various JavaScript projects.

Key features:

  • Built-in test runner: Executes tests and provides clear results, often without requiring any additional configuration.

  • Built-in mocking library: Simplifies isolating components and mocking dependencies for focused testing.

  • Built-in assertion library: Offers a range of assertions for verifying test outcomes.

  • Snapshot testing: A unique feature that captures the rendered output of UI components and compares it to a reference snapshot for detecting unintended changes.

  • Zero configuration: Often works out-of-the-box with minimal setup.

  • Fast execution: Known for its rapid test execution speeds, promoting a smooth testing experience.

  • Great developer experience: Provides clear error messages and helpful suggestions, making debugging easier.

  • Works with various frameworks: Compatible with React, Angular, Vue, Node.js, and more.

Advantages of using Jest:

  • Simplicity: Easy to learn and use, with a streamlined API and intuitive setup.

  • Speed: Fast test execution encourages frequent testing and quicker feedback loops.

  • Versatility: Handles different types of tests, including unit, integration, and snapshot tests.

  • Mocking: Simplifies testing complex components with dependencies.

  • Developer experience: Promotes a productive and enjoyable testing process.

  • Community and support: Backed by a large community and comprehensive documentation.

Setup React Testing Library

Install React Testing Library

npm i -D @testing-library/react

Install Jest

npm i -D jest 
npm i -D @testing-library/jest-dom

Configure Jest

npx jest --init

A file with the name of jest.config.js gets generated.

As of Jest 28 jest-environment-jsdom is no longer shipped by default, make sure to install it separately.

npm i -D jest-environment-jsdom

Configure Babel

npm i -D babel-jest @babel/core @babel/preset-env @babel/preset-react

Set babel configurations for imports. We have to do this because out test environment is jsdom which doesn't understand the import keyword as opposed to our browser which is used for dev/prod environment.

Add @babel/preset-react to the 'presets' section of your Babel config to enable transformation i.e. jsx support.

// .babelrc

{
  "plugins": [["transform-remove-console", { "exclude": ["error", "warn"] }]],
  "presets": [
    ["@babel/preset-env", { "targets": { "node": "current" } }],
    ["@babel/preset-react", { "runtime": "automatic" }]
  ]
}

JavaScript Objects VS JSON

Though the syntax of JSON is similar to the JavaScript object, JSON is different from JavaScript objects.

JSONJavaScript Object
The key in key/value pair should be in double quotes.The key in key/value pair can be without double quotes.
JSON cannot contain functions.JavaScript objects can contain functions.
JSON can be created and used by other programming languages.JavaScript objects can only be used in JavaScript.

Add the coverage folder to your .gitignore file.

Writing Unit Tests for Header Component to test for Logo, Cart - 0 items and Online Status

// components/__tests__/Header.test.js

import { StaticRouter } from "react-router-dom/server";
import { Provider } from "react-redux";
import { render } from "@testing-library/react";
import Header from "../Header";
import store from "../../utils/store";

test("Logo should load on rendering header", () => {
  // Load Header Component
  // get the virtual DOM object for our header component
  const header = render(
    <StaticRouter>
      <Provider store={store}>
        <Header />
      </Provider>
    </StaticRouter>
  );

  // check if logo is loaded
  const logo = header.getAllByTestId("logo")[0];
  expect(logo.src).toBe(window.location.origin + "/" + "dummy.jpg");
});

test("Online status should be green on rendering header", () => {
  // Load Header Component
  // get the virtual DOM object for our header component
  const header = render(
    <StaticRouter>
      <Provider store={store}>
        <Header />
      </Provider>
    </StaticRouter>
  );

  // check if online status is green
  const onlineStatus = header.getByTestId("online-status");
  expect(onlineStatus.innerHTML).toBe("✅");
});

test("Cart should have 0 items on rendering header", () => {
  // Load Header Component
  // get the virtual DOM object for our header component
  const header = render(
    <StaticRouter>
      <Provider store={store}>
        <Header />
      </Provider>
    </StaticRouter>
  );

  // check if cart is having 0 items
  const cart = header.getByTestId("cart");
  expect(cart.innerHTML).toBe("Cart - 0 items");
});

Writing Integration Test case for search feature on the Homepage

// components/__tests__/Search.test.js

import "@testing-library/jest-dom";
import { fireEvent, render, waitFor } from "@testing-library/react";
import { Provider } from "react-redux";
import { StaticRouter } from "react-router-dom/server";
import Body from "../Body";
import store from "../../utils/store";
import { RESTAURANT_DATA } from "../../mocks/data";
import { shimmer_card_unit } from "../../config";

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve(RESTAURANT_DATA),
  })
);

test("Shimmer should load on homepage", () => {
  const body = render(
    <StaticRouter>
      <Provider store={store}>
        <Body />
      </Provider>
    </StaticRouter>
  );
  const shimmer = body.getByTestId("shimmer");
  expect(shimmer.children.length).toBe(shimmer_card_unit);
});

test("Restaurants should load on homepage", async () => {
  const body = render(
    <StaticRouter>
      <Provider store={store}>
        <Body />
      </Provider>
    </StaticRouter>
  );

  await waitFor(() => expect(body.getByTestId("search-btn")));

  const restaurantList = body.getByTestId("restaurant-list");
  expect(restaurantList.children.length).toBe(29);
});

test("Search for string(food) on homepage", async () => {
  const body = render(
    <StaticRouter>
      <Provider store={store}>
        <Body />
      </Provider>
    </StaticRouter>
  );

  await waitFor(() => expect(body.getByTestId("search-btn")));

  const input = body.getByTestId("search-input");
  fireEvent.change(input, { target: { value: "food" } });

  const searchBtn = body.getByTestId("search-btn");
  fireEvent.click(searchBtn);

  const restaurantList = body.getByTestId("restaurant-list");
  expect(restaurantList.children.length).toBe(1);
});

Writing Integration Test case for Add to Cart flow

// components/__tests__/Menu.test.js

import "@testing-library/jest-dom";
import { fireEvent, render, waitFor } from "@testing-library/react";
import { Provider } from "react-redux";
import { StaticRouter } from "react-router-dom/server";
import Header from "../Header";
import RestaurantMenu from "../RestaurantMenu";
import store from "../../utils/store";
import { MENU_DATA } from "../../mocks/data";

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve(MENU_DATA),
  })
);

test("Add items to cart", async () => {
  const restaurantMenuPage = render(
    <StaticRouter>
      <Provider store={store}>
        <Header />
        <RestaurantMenu />
      </Provider>
    </StaticRouter>
  );

  await waitFor(() => expect(restaurantMenuPage.getByTestId("menu")));

  const addBtn = restaurantMenuPage.getAllByTestId("addBtn");

  fireEvent.click(addBtn[0]);
  fireEvent.click(addBtn[2]);

  const cart = restaurantMenuPage.getByTestId("cart");
  expect(cart.innerHTML).toBe("Cart - 2 items");
});

test("Remove items from cart", async () => {
  const restaurantMenuPage = render(
    <StaticRouter>
      <Provider store={store}>
        <Header />
        <RestaurantMenu />
      </Provider>
    </StaticRouter>
  );

  await waitFor(() => expect(restaurantMenuPage.getByTestId("menu")));

  const addBtn = restaurantMenuPage.getAllByTestId("addBtn");
  const removeBtn = restaurantMenuPage.getAllByTestId("removeBtn");

  fireEvent.click(addBtn[0]);
  fireEvent.click(addBtn[1]);
  fireEvent.click(addBtn[2]);

  const cart = restaurantMenuPage.getByTestId("cart");
  expect(cart.innerHTML).toBe("Cart - 3 items");

  fireEvent.click(removeBtn[0]);

  expect(cart.innerHTML).toBe("Cart - 2 items");
});

That's all for now folks. See you in the next blog!