Skip to content

Unit Testing

vitesttesting-librarytestcontainersdocker

Running real databases in parallel Best Practice

Using a real database is more robust than mocking one. However, using an actual database makes parallel execution challenging. This template leverages Testcontainers to assign random ports, enabling parallel execution of test suites.

As a result, this template were able to significantly reduce the overall test completion time while using a real database.

ts
import type { User } from "next-auth";
import { afterAll, afterEach, expect, vi } from "vitest";

export async function setup() {
  const { container, prisma, truncate, down } = await vi.hoisted(async () => {
    const { setupDB } = await import("../../../tests/db.setup");

    return await setupDB({ port: "random" });
  });

  const mock = vi.hoisted(() => ({
    auth: vi.fn(),
    revalidatePath: vi.fn(),
    revalidateTag: vi.fn(),
  }));

  vi.mock("../_clients/prisma", () => ({
    prisma,
  }));

  vi.mock("next-auth", () => ({
    default: () => ({
      auth: mock.auth,
    }),
  }));

  vi.mock("next/cache", () => ({
    revalidatePath: mock.revalidatePath,
    revalidateTag: mock.revalidateTag,
  }));

  afterAll(async () => {
    await down();
  });

  afterEach(async () => {
    await truncate();
  });

  async function createUser() {
    const user: User = {
      id: "id",
      name: "name",
      email: "hello@a.com",
      image: "https://a.com",
      role: "user",
    };

    mock.auth.mockReturnValue({
      user,
    });

    await prisma.user.create({
      data: user,
    });

    expect(await prisma.user.count()).toBe(1);

    return user;
  }

  return <const>{
    container,
    prisma,
    truncate,
    down,
    mock,
    createUser,
  };
}
ts
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { Prisma, PrismaClient } from "@prisma/client";
import { DockerComposeEnvironment, Wait } from "testcontainers";
import { createDBUrl } from "../src/app/_utils/db";

const execAsync = promisify(exec);

export async function setupDB({ port }: { port: "random" | number }) {
  const container = await new DockerComposeEnvironment(".", "compose.yml")
    .withEnvironmentFile(".env.test")
    // overwrite environment variables
    .withEnvironment({
      DATABASE_PORT: port === "random" ? "0" : `${port}`,
    })
    .withWaitStrategy("db", Wait.forListeningPorts())
    .up(["db"]);
  const dbContainer = container.getContainer("db-1");
  const mappedPort = dbContainer.getMappedPort(5432);
  const url = createDBUrl({
    host: dbContainer.getHost(),
    port: mappedPort,
  });

  await execAsync(`DATABASE_URL=${url} npx prisma db push`);

  const prisma = new PrismaClient({
    datasources: {
      db: {
        url,
      },
    },
  });

  async function down() {
    await prisma.$disconnect();
    await container.down();
  }

  return <const>{
    container,
    port,
    prisma,
    truncate: () => truncate(prisma),
    down,
    async [Symbol.asyncDispose]() {
      await down();
    },
  };
}

export async function truncate(prisma: PrismaClient) {
  const tableNames = Prisma.dmmf.datamodel.models.map((model) => {
    return model.dbName || model.name.toLowerCase();
  });
  const truncateQuery = `TRUNCATE TABLE ${tableNames.map((name) => `"${name}"`).join(", ")} CASCADE`;

  await prisma.$executeRawUnsafe(truncateQuery);
}

IMPORTANT

When using this feature with Vitest, you need to perform a dynamic import within vi.hoisted. Please check test.helper.ts