Passa al contenuto principale

Testing

ProBeya uses a layered testing strategy with unit tests, integration tests, and end-to-end tests. This guide explains the tools, patterns, and best practices.

Testing Stack​

ToolPurpose
VitestUnit and integration test runner
Testing LibraryReact component testing
PlaywrightEnd-to-end browser testing
MSWAPI mocking for frontend tests
TestcontainersEphemeral PostgreSQL and Redis for integration tests

Running Tests​

# Run all tests
pnpm test

# Run tests for a specific package
pnpm test --filter=@probeya/api

# Run tests in watch mode
pnpm test:watch

# Run end-to-end tests
pnpm test:e2e

# Generate coverage report
pnpm test:coverage

Unit Tests​

Unit tests verify individual functions and modules in isolation. They are fast and do not require external services.

Example​

// packages/validators/src/__tests__/item-schema.test.ts
import { describe, it, expect } from "vitest";
import { createItemSchema } from "../item-schema";

describe("createItemSchema", () => {
it("accepts valid input", () => {
const result = createItemSchema.safeParse({
boardId: "brd_abc123",
groupId: "grp_abc123",
title: "Test item",
fields: { status: "todo" },
});
expect(result.success).toBe(true);
});

it("rejects empty title", () => {
const result = createItemSchema.safeParse({
boardId: "brd_abc123",
groupId: "grp_abc123",
title: "",
fields: {},
});
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.path).toEqual(["title"]);
});

it("rejects title exceeding max length", () => {
const result = createItemSchema.safeParse({
boardId: "brd_abc123",
groupId: "grp_abc123",
title: "a".repeat(501),
fields: {},
});
expect(result.success).toBe(false);
});
});

Integration Tests​

Integration tests verify tRPC procedures against a real database. We use Testcontainers to spin up ephemeral PostgreSQL instances:

// packages/api/src/routers/items.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { createTestContext } from "../test-utils/context";

describe("item procedures", () => {
let ctx: Awaited<ReturnType<typeof createTestContext>>;

beforeAll(async () => {
ctx = await createTestContext();
});

afterAll(async () => {
await ctx.teardown();
});

it("creates and retrieves an item", async () => {
const created = await ctx.caller.item.create({
boardId: ctx.board.id,
groupId: ctx.group.id,
title: "Integration test item",
fields: { status: "todo" },
});

expect(created.id).toBeDefined();
expect(created.title).toBe("Integration test item");

const retrieved = await ctx.caller.item.byId({ id: created.id });
expect(retrieved.title).toBe("Integration test item");
expect(retrieved.fields.status).toBe("todo");
});

it("enforces organization isolation", async () => {
const otherCtx = await createTestContext(); // Different org

await expect(
otherCtx.caller.item.byId({ id: ctx.item.id }),
).rejects.toThrow("NOT_FOUND");

await otherCtx.teardown();
});
});

End-to-End Tests​

E2E tests use Playwright to test the full application in a real browser:

// apps/web/e2e/board.spec.ts
import { test, expect } from "@playwright/test";

test.describe("Board View", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/login");
await page.fill("[name=email]", "[email protected]");
await page.fill("[name=password]", "password");
await page.click("button[type=submit]");
await page.waitForURL("/dashboard");
});

test("creates a new item via quick add", async ({ page }) => {
await page.goto("/workspace/engineering/project/sprint/board/main");
await page.click("text=+ Add Item");
await page.fill("[data-testid=item-title-input]", "New E2E item");
await page.keyboard.press("Enter");
await expect(page.locator("text=New E2E item")).toBeVisible();
});

test("drags an item between groups", async ({ page }) => {
await page.goto("/workspace/engineering/project/sprint/board/main");
const item = page.locator("[data-testid=item-card]").first();
const targetGroup = page.locator("[data-testid=group-drop-zone]").nth(1);
await item.dragTo(targetGroup);
// Verify the item moved
await expect(targetGroup.locator("text=Dragged item")).toBeVisible();
});
});

Coverage Requirements​

We aim for the following coverage targets:

LayerTarget
Validators95%+
API procedures85%+
Database queries80%+
React components70%+
E2E critical pathsAll major user flows

Coverage reports are generated by pnpm test:coverage and displayed in CI.

Best Practices​

  1. Test behavior, not implementation. Test what a function does, not how it does it.
  2. Use descriptive test names. A failing test name should tell you what broke.
  3. Keep tests independent. Each test should set up its own state and clean up after itself.
  4. Prefer integration tests for API procedures — they catch more real-world bugs than unit tests alone.
  5. Use factories to create test data instead of hardcoding values.
  6. Do not test framework code. Trust that React, tRPC, and Drizzle work correctly.