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
| Tool | Purpose |
|---|---|
| Vitest | Unit and integration test runner |
| Testing Library | React component testing |
| Playwright | End-to-end browser testing |
| MSW | API mocking for frontend tests |
| Testcontainers | Ephemeral 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=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:
| Layer | Target |
|---|---|
| Validators | 95%+ |
| API procedures | 85%+ |
| Database queries | 80%+ |
| React components | 70%+ |
| E2E critical paths | All major user flows |
Coverage reports are generated by pnpm test:coverage and displayed in CI.
Best Practices
- Test behavior, not implementation. Test what a function does, not how it does it.
- Use descriptive test names. A failing test name should tell you what broke.
- Keep tests independent. Each test should set up its own state and clean up after itself.
- Prefer integration tests for API procedures — they catch more real-world bugs than unit tests alone.
- Use factories to create test data instead of hardcoding values.
- Do not test framework code. Trust that React, tRPC, and Drizzle work correctly.