Skip to main content

Testing

This application has the following test types set up:

  • integration (via Vitest)
  • end-to-end tests (via Playwright)
  • client interaction testing (via Storybook and Chromatic)
  • client visual regression testing (via Storybook and Chromatic)

This section will cover the "backend" tests (integration and e2e), as well as how to write new tests for tRPC procedures. For more information on the client tests, refer to the Storybook and Chromatic sections.

Integration tests

The integration tests use Vitest. The groundwork has been laid out to ensure that the tests are set up to run with the correct environment variables and database connection, and has also been verified to work with testing tRPC procedures.

info

"Why Vitest? Why not Jest?" you may ask. Basically because it is faster. Read Vitest's comparison page.

Everything has already been set up in the application. The main files to look at are:

FileDescription
vitest.config.tsThis is the Vitest configuration file.
.env.testEnvironment variables specifically for testing

Running Vitest (locally)

As this is an integration test suite that relies on a working database connection (rather than mocking the database), you will need to spin up the database before running the tests. This can be done with:

npm run setup

to spin up the database in a Docker container.

Then, run the tests with: npm run test-dev:unit for watch mode or npm run test:unit for a single run.

The test scripts have been set up to use the .env.test file, which has been configured to use the test database connection (specifically the test database exposed localhost:26257).

Setup and teardown of Prisma per test

Advanced: How tests run Prisma queries in parallel

The application uses a Vitest helper package vitest-environment-vprisma, which improves the experience of testing with vitest and @prisma/client. It allows you to isolate each test case with a transaction and rollback after completion, giving you a safe and clean testing environment.

A Prisma mock has been set up in the application that converts all Prisma Client usage in tests to use the mock. This is done in vitest.setup.ts:

vitest.setup.ts
import { vi } from "vitest";

vi.mock("./src/server/prisma", () => ({
prisma: vPrisma.client,
}));

You can then use the prisma object in your tests as you would normally.

src/server/modules/auth/email/__tests__/email.router.test.ts
import { prisma } from "~/server/prisma";
// ...
await prisma.verificationToken.create({
data: {
expires: new Date(Date.now() + env.OTP_EXPIRY * 1000),
identifier: TEST_EMAIL,
token: VALID_TOKEN_HASH,
},
});

Writing new integration tests for tRPC procedures

You can easily set the tRPC context using the createContextInner function exposed in src/server/context.ts. The application also includes helper functions that you can use to set up the context for your tests in tests/integration/helpers/iron-session.ts,

Function nameDescription
applySessionCreate a mock IronSession object that you can use to set up an unauthenticated session.
applyAuthedSessionCreate a mock IronSession object that you can use to set up an authenticated session.

Which can be used as follows:

import { createContextInner } from "~/server/context";
import {
applyAuthedSession,
applySession,
} from "tests/integration/helpers/iron-session";

// Unauthorized session example
describe("auth.email router", () => {
describe("login procedure", () => {
it("should throw if email is not provided", async () => {
// Arrange
const session = applySession();
const ctx = await createContextInner({ session });
const caller = emailSessionRouter.createCaller(ctx);

// Act
// Call procedure with empty email
const result = caller.login({ email: "" });

// Assert
await expect(result).rejects.toThrowError();
});
});
});

// Authorized session example
describe("post router", () => {
describe("add procedure", () => {
it("post should be retrievable after creation", async () => {
const defaultUser: User = {
/* ... */
};
const session = await applyAuthedSession(defaultUser);
const ctx = await createContextInner({
session,
});
const caller = postRouter.createCaller(ctx);

const input: RouterInput["post"]["add"] = {
title: "hello test",
content: "hello test with a long input",
contentHtml: "<p>hello test with a long input</p>",
};

const post = await postRouter.add(input);
const byId = await postRouter.byId({ id: post.id });

expect(byId).toMatchObject(input);
});
});
});

End-to-end (E2E) tests

E2E tests uses Playwright. Tests are located in the playwright folder.

Running Playwright (locally)

As this is an e2e test suite, both the application and database needs to be active. This can be done with:

npm run setup
npm run dev

to spin up the database in a Docker container, as well as run the Next.js application.

Then, run the e2e tests with: nm run test:e2e, which will spin up the Playwright test runner.

The test script has been set up to use the .env.test file, which has been configured to use the test database connection (specifically the test database exposed localhost:26257).

Writing new E2E tests

Playwright provides a test function to declare tests and expect function to write assertions, similar to Vitest (and other test runners).

In e2e tests, you can use the page object to interact with the browser. For example, to go to the home page and check that the app name is displayed:

import { test, expect } from "@playwright/test";
import { env } from "~/env.mjs";

test("go to /", async ({ page }) => {
await page.goto("/");

await page.waitForSelector(`text=${env.NEXT_PUBLIC_APP_NAME}`);
});

The runner also exposes the request and response objects after an action, which you can use to make assertions on the HTTP requests made by the application. For example, to check that the 404 page returns a 404 status code:

test("test 404", async ({ page }) => {
const res = await page.goto("/not-found");
expect(res?.status()).toBe(404);
});

Everything has already been set up in the application. The main files to look at are:

FileDescription
playwright.config.tsThis is the Playwright configuration file.
.env.testEnvironment variables specifically for testing
playwright/**.test.tsThe E2E tests

Useful Resources

ResourceLink
Vitest confighttps://vitest.dev/config/
Writing Playwright Testshttps://playwright.dev/docs/writing-tests