Unit Testing React Applications in a NX (NRWL) Monorepo with Vitest
Published: 2023-09-13 12:33:05

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

```

## 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
```

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
```

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
```


## 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
```

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.

```ts
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 });
```


## 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.

```ts
/// <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",
      },
    },
  },
});
```

## 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.

```ts
// processRequest.ts
export const processRequest = (request) => {
  console.log("request", request);
};
```

```ts
// 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();
  });
});
```

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

```
npx nx run react-app-ui:test
```

### Step 2. Create simple React Component and test

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

```tsx
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>
  );
};
```

Write a simple Spec for the Component:

```tsx
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
  });
});
```


## 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