Table of contents
- Why do we need test cases?
- What are different types for testing?
- What is Enzyme?
- Enzyme vs React Testing Library
- What is Jest and why do we use it?
- Setup React Testing Library
- Writing Unit Tests for Header Component to test for Logo, Cart - 0 items and Online Status
- Writing Integration Test case for search feature on the Homepage
- Writing Integration Test case for Add to Cart flow
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
Feature | Enzyme | React Testing Library |
Approach | Focuses on component implementation details | Focuses on user experience and rendered output |
Rendering | Offers shallow, full rendering, and static | Uses full rendering only |
DOM Access | Direct access to component internals | Encourages querying by accessibility roles, labels, text content |
Event Simulation | Provides specific methods for triggering events | Uses fireEvent for user-like interactions |
State/Props | Allows direct manipulation of state and props | Encourages testing based on rendered output, not internal state |
Assertions | Uses Jest's built-in assertions | Often paired with jest-dom for custom matchers |
Maintained by | Airbnb | Kent 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.
JSON | JavaScript 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!