DEV Community

Shannon Lal
Shannon Lal

Posted on

Unit Testing React Applications in a NX (NRWL) Monorepo with Vitest

Testing is the foundation that enables sustainable growth in any software project. For React applications in a NX monorepo, comprehensive unit testing across all projects is essential for stability as the codebase expands. However, slow and flaky tests can grind development velocity to a crawl by bogging down commits in the CI.

Vitest is a blazing fast unit test runner with first class React support that brings testing joy back to your monorepo. It smartly executes tests in parallel for unrivaled speed while remaining a tiny dependency.

In this post, we’ll explore setting up Vitest in a NX workspace with support for Canvas and writing fast unit tests for React components. With Vitest and NX, you can implement comprehensive unit testing to gain confidence as your apps grow.

We’ll assume you have working knowledge of TypeScript, React, testing, and NX workspaces. Ready to unlock the power of speedy unit testing for your monorepo? Let’s dive in!

Project setup

The following is an example of the monorepo structure we use at Designstripe and what we will be using during this blog.

Workspace Name: react-nrwl
Application: react-app-ui — the main React application
Shared Library: common-ui — contains shared React components bundled with Vite

Application Structure

apps/
  react-app-ui/
    src/
      pages/
      components/

    next.config.js
    project.json
    tsconfig.ts
    tsconfig.spec.ts
    test-setup.ts
    vite.config.ts

libs/
  common-ui/
    src/
      lib/
        components/
          IconAlert.tsx
      index.ts
    project.json
    tsconfig.json
    tsconfig.spec.json
    vite.config.ts
nx.json
tailwind.config.js
tsconfig.base.json
package.json

Enter fullscreen mode Exit fullscreen mode

Use NX Vite plugin to configure your application

First, if you haven’t already, install the NX Vite plugin. This plugin will allow you to generate Vite applications and libraries in your NX workspace.

npm install -D @nrwl/vite
Enter fullscreen mode Exit fullscreen mode

Next, use the NX generator to add Vitest to your workspace. This will add the Vitest configuration, update the test target in your project.json and update the tsconfig.json to include the Vitest types.

npx nx g @nrwl/vite:vitest --project=react-app-ui --testEnvironment=jsdom --uiFramework=react
Enter fullscreen mode Exit fullscreen mode

File changes:

UPDATE apps/react-app-ui/project.json
CREATE apps/react-app-ui/vite.config.ts
CREATE apps/react-app-ui/tsconfig.spec.json
UPDATE apps/react-app-ui/tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Install testing dependencies for React, Vite and Canvas

npm install -D @testing-library/react @testing-library/jest-dom @vitejs/plugin-react vite-tsconfig-paths vitest-canvas-mock
Enter fullscreen mode Exit fullscreen mode

That's a lot of dependencies, so let's break them down:

  • @testing-library/react: A library for testing React components. It provides utilities to simulate user interactions and query elements in a way that's more aligned with how users interact with your app.

  • @testing-library/jest-dom: A set of custom matchers that makes it easier to assert how your components should behave in the DOM.

  • @vitejs/plugin-react: The official Vite plugin to support React. This plugin enables features like Fast Refresh in a Vite + React development setup.

  • vite-tsconfig-paths: A Vite plugin that enables support for TypeScript's paths and baseUrl configuration options. This is useful when you want to set up custom path aliases in a TypeScript project using Vite.

  • vitest-canvas-mock: Mocks the canvas API when running unit tests with vitest.

Add a test setup file

We can optionally add a test setup file to our project. This file will be executed before each test file and can be used to configure the testing environment. In ours, we use it to mock the canvas API and some other global functions that are used in our tests. We also clean up the DOM after each test using the cleanup function from @testing-library/react.

import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
// Option for using mocking canvas testing
import "vitest-canvas-mock";

// runs a cleanup after each test case (e.g. clearing jsdom)
afterEach(() => {
  cleanup();
});

// Add Default Functions
/* eslint-disable @typescript-eslint/no-empty-function */
const noop = () => {};
Object.defineProperty(window, "scrollTo", { value: noop, writable: true });
Enter fullscreen mode Exit fullscreen mode

Update Vite Config to support test setup and reference libraries in MonoRepo

One of the challenges with setting up testing using Vitest is that Vite doesn't know how to resolve the paths to the libraries in the monorepo by default. To resolve this, we need to add a Vite plugin called vite-tsconfig-paths. This plugin will read the tsconfig.json file and resolve the paths to the libraries in the monorepo. We also make sure that the Vite React plugin is configured, and we add the test setup file to the Vite config.

/// <reference types="vitest" />
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import viteTsConfigPaths from "vite-tsconfig-paths";
import dts from "vite-plugin-dts";
import { join } from "path";

export default defineConfig({
  plugins: [
    // This is required to build the test files with SWC
    react({ include: /\.(js|jsx|ts|tsx)$/, fastRefresh: false }),
    viteTsConfigPaths({
      root: "../../",
    }),
  ],

  test: {
    globals: true,
    cache: {
      dir: "../../node_modules/.vitest",
    },
    environment: "jsdom",
    deps: {
      // Required for vitest-canvas-mock
      inline: ["vitest-canvas-mock"],
    },
    include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
    setupFiles: "./test-setup.ts",
    // Required for vitest-canvas-mock
    threads: false,
    environmentOptions: {
      jsdom: {
        resources: "usable",
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Create Unit Tests

Step 1. Create simple Typescript function and test

Here we create a simple function that we will test to validate that Vitest is working.

// processRequest.ts
export const processRequest = (request) => {
  console.log("request", request);
};
Enter fullscreen mode Exit fullscreen mode
// processRequest.spec.ts
// Note: Simple test to validate that vitest is working
import { processRequest } from "./processRequest";

describe("processRequest", () => {
  it("Calls Function with process request", () => {
    expect(processRequest).toBeDefined();
  });
});
Enter fullscreen mode Exit fullscreen mode

To validate that Vitest is working, we can run the test using the following command:

npx nx run react-app-ui:test
Enter fullscreen mode Exit fullscreen mode

Step 2. Create simple React Component and test

Note: It references a component that is in a library in the mono repo

import { processRequest } from "./processRequest";
// Note this behind the scene references the canvas
import { IconAlert } from "@react-nrwl/common-ui";

interface ButtonProps {
  label: string;
}

export const Button = ({ label }: ButtonProps) => {
  const onClick = () => {
    scrollTo({ top: 0, behavior: "smooth" });
    processRequest(label);
  };

  return (
    <div>
      <IconAlert />
      <button
        data-testid="focusedButton"
        onClick={onClick}
        type="button"
        className="bg-purple/100 flex px-4 py-3 rounded-[12px] text-12 leading-6 font-medium text-left text-purple/700 sm-down:w-[100%] duration-500 hover:bg-neutral-200 transition ease-in-out-expo"
      >
        {label}
      </button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Write a simple Spec for the Component:

import { render, screen, fireEvent } from "@testing-library/react";
import { vi, Mock } from "vitest";

/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-ignore
global.console = { log: vi.fn() };

import { Button } from "./Button";

describe("Button", () => {
  it("renders headline", () => {
    const consoleSpy = vi.spyOn(global.console, "log");
    const comp = render(<Button label={"Test Label"} />);

    const button = screen.getByTestId("focusedButton");
    expect(button).toBeDefined();

    fireEvent.click(button);

    expect(consoleSpy).toHaveBeenCalledTimes(1);
    screen.debug();

    expect(comp).toBeDefined();
    // check if App components renders headline
  });
});
Enter fullscreen mode Exit fullscreen mode

Parting Thoughts

In this post, we explored configuring Vitest for unit testing React apps in NX monorepos. With the setup covered, you're equipped to implement comprehensive unit testing.

Testing is crucial as complexity grows. But slow, flaky tests stall velocity. Vitest delivers the speed and reliability needed to test React frequently.

Pair Vitest with NX workspaces for scale. Write unit tests early and often to prevent bugs. Testing with confidence and joy enables sustainable development.

We hope you found this guide helpful. Please let us know if you have any other questions—we're happy to help. And feel free to share your own React testing tips in the comments below!

Authors

Jeff Schofield

https://www.linkedin.com/in/jeff-schofield-76555b163/
https://twitter.com/JeffScript

Shannon Lal

https://www.linkedin.com/in/shannonlal/
https://twitter.com/shannondlal

Top comments (0)