> ## Content Index
> Fetch the complete content index at: https://madewithlove.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# “Fake” e2e tests with react-testing-library
- URL: https://madewithlove.com/blog/fake-e2e-tests-with-react-testing-library/
- Published: 2024-04-24T08:51:34.000Z
- Updated: 2026-04-21T20:15:48.000Z
- Description: How to run fake e2e tests using react-testing-librabry? Here's a quick tutorial on how to speed up your feedback loop when working in front end applications.
- Author: Geoffrey Dhuyvetters
- Tags: Engineering, Code quality, Testing, Tutorials, JavaScript, React, Frontend

I love end-to-end (e2e) tests in [Playwright](https://playwright.dev/) or [Cypress](https://www.cypress.io/). They are the best way to test a codebase from a user's perspective. But they do have downsides. They can be a pain to set up, hard to maintain, and potentially take minutes to run. 

Software engineering is all about fast feedback loops. I've been using a new approach where I mount Next.js pages in [react-testing-library](https://testing-library.com/docs/react-testing-library/intro/) and run fake e2e tests.

There is no headless browser involved. This significantly increases the speed of the feedback loop. You can also do this using [Vue Testing Library](https://testing-library.com/docs/vue-testing-library/intro/).

Here’s the full example. We’ll go through it line by line below.

```JavaScript
setupMockApi([
  {
    url: "/todo/9eb4948387",
    handler: (req, res, ctx) => {
      return res(ctx.status(200), ctx.json(mockResponse));
    },
  }
])

vi.mock("next/router", () => ({
  useRouter: vi.fn(),
}));

// create a spy on a function
const pushMock = vi.fn();

beforeEach(() => {
  pushMock.mockReset();
});

describe("Todo Detail Page", () => {
    test("Should be able to remove a todo", async () => {
      useRouter.mockReturnValue({
        query: { id: "9eb4948387" }, // id is used on the data fetch
        push: pushMock,
      });
    
      const { findByRole, queryByRole } = renderComponentWithWrapper(
        <TodoDetailPage />
      );
    
      const deleteButton = await findByRole("button", { name: "actions.delete" });
      await userEvent.click(deleteButton);
    
      const confirmDeleteButton = await findByRole("button", {
        name: "actions.confirm",
      });
      await userEvent.click(confirmDeleteButton);
    
      expect(pushMock).not.toHaveBeenCalled();
      expect(queryByRole("alertdialog")).not.toBeInTheDocument();
    });
})
```

I've created a setupMockApi function for convenience. I use [msw](https://mswjs.io/) to mock API calls. This allows me to return the values that I want.

```JavaScript
// defaults to get
// with a default base URL

setupMockApi([
  {
    url: `/images `,
    method: "post",
    handler: (req, res, ctx) => {
      return res(ctx.status(200), ctx.json(imagesPostResult));
    },
  },
  {
    url: `/images`,
    handler: (req, res, ctx) => {
      return res(ctx.status(200), ctx.json(imagesGetResult));
    },
  },
]);
```

Next, I mock the router. I created a pushMock object to use instead of push. In the test, this is defined as a parameter to useRouter. I also reset the pushMock object before it’s used.

```JavaScript
const pushMock = vi.fn();

beforeEach(() => {
  pushMock.mockReset();
});

// in a test
useRouter.mockReturnValue({
  query: { id: "3def30ab-4048-4d72-9014-60215444a96d" },
  push: pushMock,
});

// another test
// assert if push was called with a specific route
expect(pushMock).toHaveBeenCalledWith("/specific/url")
```

The next line is where some magic happens. Create your own method that wraps a page with the required Providers (as needed by tanstack-query, next-auth, and/or custom providers).

```JavaScript
export const Wrapper = ({ children }) => {
  const queryClient = useMemo(() => {
    return new QueryClient({
      defaultOptions: {
        queries: {
          retry: false,
        },
      },
      logger: {
        log: console.log,
        warn: console.warn,
        error: process.env.NODE_ENV === "test" ? () => {} : console.error,
      },
    });
  }, []);

  return (
    <QueryClientProvider client={queryClient}>
      <SessionProvider session={session}> // mock next-auth session
        {children}
      </SessionProvider>
    </QueryClientProvider>
  );
};

const renderComponentWithWrapper = (
  Component,
  options
) =>
  render(Component, {
    wrapper: Wrapper,
    ...options,
  });

// Usage in a specific test
const { findByRole } = renderComponentWithWrapper(
    <SomePage />
);
// describe test scenarios
```

Writing queries using \`findBy\[...\]\` is key. Use [userEvent](https://testing-library.com/docs/user-event/intro) for (you guessed it) user-triggered events. React-testing-library has [a great resource](https://testing-library.com/docs/queries/about/#priority) regarding writing queries

## Tips for testing end-to-end without a headless browser

Here are some pointers on setting up tests like this. First, configure [pageExtensions](https://nextjs.org/docs/pages/api-reference/next-config-js/pageExtensions) in next.config.js to only load pages that end with *page.tsx*. This allows you to collocate tests with their respective pages.

```JavaScript
// next.config.js
pageExtensions: ["page.tsx"],

// folder structure example
/src
    /pages
      /index.page.tsx
      /index.test.ts
```

Set up translations in your global test setup file. Prioritize testing using translation keys, the default fallback for most translation systems. This decouples the translations from the tests.

```JavaScript
const button = await findByRole("button", { name: "action.confirm" });
```

Debugging can be tricky at moments; using [.debug() ](https://testing-library.com/docs/queries/about/#screendebug)can greatly help you. This method outputs the current HTML at a specific point in a test.

Finally, this example is in Next.js using Vitest, but this approach can be used with pretty much every front-end framework or testing suite, such as Jest.

Happy experimenting!