Use unit tests for focused behavior, integration tests for collaborating infrastructure, and end-to-end tests for critical user flows.
Course navigationPhase 28⌄
Testing
Build a testing strategy that proves pure rules, service orchestration, repositories, HTTP contracts, frontend behavior, and critical browser flows at the right cost.
Phase outcome and project increment
Learning outcome
Build a testing strategy that proves pure rules, service orchestration, repositories, HTTP contracts, frontend behavior, and critical browser flows at the right cost.
Concrete deliverable
Vitest + integration + Playwright test suite with deterministic fixtures and CI-ready commands.
Principles to keep while coding
Test business outcomes and contracts rather than coupling every test to private implementation details.
Keep tests deterministic by controlling clocks, random values, external services, and database state where necessary.
A useful test suite catches regressions while remaining fast enough to run continuously in development and CI.
Guided phase lab
Do these in order. Each step should end with evidence before the next one starts.
- 01
List the highest-risk TaskFlow behaviors before choosing test counts.
- 02
Write a pure unit test for one deterministic business policy.
- 03
Test a service with controlled dependencies and assert business outcome rather than private calls.
- 04
Run a repository integration test against real PostgreSQL with isolated data.
- 05
Run an API test through NestJS for validation/auth/error contract.
- 06
Write one Playwright flow for a critical user journey and make setup/cleanup reproducible.
pnpm test
pnpm --filter api test
pnpm --filter web test
pnpm exec playwright testPredict what each command proves before running it. A command is useful only when its output answers a question.
Failures you should know how to diagnose
Mocking the thing the test is supposed to prove
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Huge number of brittle snapshot/UI implementation tests
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Shared mutable test data causing order-dependent failures
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
E2E used for every rule, making feedback too slow
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Lessons
The source chapters come first. Every phase now ends with a guided build, a debugging workshop, and an independent challenge so reading turns into transferable skill.
Testing Strategy and Confidence Pyramid
Testing Strategy and Confidence Pyramid TaskFlow - Full Stack Developer 2026 Testing is risk management, not line counting Test the boundary you are trying to prove Pure rule Service Reposit
Vitest Monorepo Setup and Test Commands
Vitest Monorepo Setup and Test Commands TaskFlow - Full Stack Developer 2026 Why Vitest now NestJS 12 newly generated projects use Vitest by default, while Next.js provides an official Vites
Pure Business Logic Unit Tests
Pure Business Logic Unit Tests TaskFlow - Full Stack Developer 2026 Extract deterministic rules first The easiest test is often enabled by the best code shape. A pure policy function accepts
NestJS Service Tests with Fake Repositories
NestJS Service Tests with Fake Repositories TaskFlow - Full Stack Developer 2026 What this layer proves TasksService tests prove application orchestration: which repository calls happen, how
Prisma Repository Integration Tests with PostgreSQL
Prisma Repository Integration Tests with PostgreSQL TaskFlow - Full Stack Developer 2026 Why this layer must be real A Prisma repository exists specifically to encode persistence behavior. I
NestJS HTTP E2E Authentication and Authorization
NestJS HTTP E2E: Authentication and Authorization TaskFlow - Full Stack Developer 2026 What the HTTP suite proves This layer exercises the assembled Nest request path: CORS-independent HTTP
React Component and Form Testing
React Component and Form Testing TaskFlow - Full Stack Developer 2026 Test through user-visible behavior React Testing Library encourages tests that resemble how users find and operate the U
TanStack Query Testing and Cache Isolation
TanStack Query Testing and Cache Isolation TaskFlow - Full Stack Developer 2026 Never share a QueryClient across tests function createTestQueryClient() { return new QueryClient({ defaultOpti
Playwright Critical End to End Workflows
Playwright Critical End-to-End Workflows TaskFlow - Full Stack Developer 2026 What belongs in browser E2E Critical TaskFlow user journey Browser Next.js NestJS Prisma Postgres sign in form a
Fixtures Factories Determinism and Isolation
Fixtures, Factories, Determinism and Isolation TaskFlow - Full Stack Developer 2026 A test fixture is a scenario, not a dump of production data Build the smallest domain graph that explains
Coverage Test Quality and CI Readiness
Coverage, Test Quality and CI Readiness TaskFlow - Full Stack Developer 2026 Coverage is a signal, not a target game • High line coverage can still miss authorization mistakes, bad assertion
Hands On Lab and Phase Exit
Hands-On Lab and Phase Exit TaskFlow - Full Stack Developer 2026 Lab 1 - Pure policy • Write table-driven tests for every allowed and rejected Task status transition. • Add one deterministic
Guided Build Lab
Purpose Build a testing strategy that proves pure rules, service orchestration, repositories, HTTP contracts, frontend behavior, and critical browser flows at the right cost.
Debugging Workshop
Goal Learn to diagnose this phase's failures systematically instead of reinstalling packages, changing random configuration, or copying a fix without understanding it.
Phase Synthesis & Independent Challenge
Mission Rebuild the capability of Phase 28 without following the exact code line-by-line. The goal is transfer: can you use the ideas when the variable names, UI, or route shape changes?
Mastery Capstone Lab
Integrate Phase 28 without following a transcript.
Exercises that increase independence
List the highest-risk TaskFlow behaviors before choosing test counts.
Test a service with controlled dependencies and assert business outcome rather than private calls.
reproduce and diagnose — Mocking the thing the test is supposed to prove.
deliver Vitest + integration + Playwright test suite with deterministic fixtures and CI-ready commands.
Can choose test level by risk and Can write deterministic tests.
Do not leave the phase only because you reached the bottom
Phase exit criteria
Scenario and concept quiz
These questions test engineering choices rather than whether you remember a lesson title.
Reference files
Attempt the work first. Use snapshots to compare a checkpoint, not as a substitute for understanding.
reference/TaskFlow_Testing_Snapshot/README.md
# TaskFlow Phase 28 Testing Snapshot This snapshot demonstrates the intended testing boundaries. It is reference code, not a wholesale replacement for every existing file. Test layers: 1. Pure unit policies 2. Nest service tests with fake repositories 3. Prisma repository integration tests with a dedicated PostgreSQL database 4. Nest HTTP e2e tests 5. React/TanStack component tests 6. Playwright critical browser journeys
reference/TaskFlow_Testing_Snapshot/SOURCES.md
- NestJS Testing: https://docs.nestjs.com/fundamentals/testing - Prisma ORM v7 Testing: https://www.prisma.io/docs/orm/v7/more/best-practices - Prisma Unit Testing: https://docs.prisma.io/docs/orm/prisma-client/testing/unit-testing - Next.js Vitest: https://nextjs.org/docs/app/guides/testing/vitest - Next.js Playwright: https://nextjs.org/docs/app/guides/testing/playwright - React Testing Library: https://testing-library.com/docs/react-testing-library/intro/ - TanStack Query Testing: https://tanstack.com/query/latest/docs/framework/react/guides/testing - Playwright: https://playwright.dev/docs/browsers - Vitest npm: https://www.npmjs.com/package/vitest - React Testing Library npm: https://www.npmjs.com/package/@testing-library/react - Playwright npm: https://www.npmjs.com/package/@playwright/test
reference/TaskFlow_Testing_Snapshot/TESTING_STRATEGY.md
# Testing strategy - Pure rules: no framework, no database. - TasksService: FakeTaskRepository. - PrismaTaskRepository: real PostgreSQL. - API security/contracts: boot Nest application, use Supertest. - React components/forms: Vitest + Testing Library + jsdom. - Query behavior: isolated QueryClient per test. - Critical user journeys: Playwright. Do not mock the dependency whose real behavior is the subject of the test.
reference/TaskFlow_Testing_Snapshot/VERSION_LOCK.md
- vitest: 5.0.1 - @nestjs/testing: 12.0.3 - supertest: 7.2.2 - @testing-library/react: 16.3.3 - @testing-library/dom: 10.4.2 - @testing-library/jest-dom: 7.0.1 - @testing-library/user-event: 14.6.7 - jsdom: 30.1.0 - @playwright/test: 1.63.0
reference/TaskFlow_Testing_Snapshot/apps/api/src/tasks/task-status.policy.spec.ts
import { describe, expect, it } from "vitest";
import { assertTaskStatusTransition, TaskStatusTransitionNotAllowed } from "./task-status.policy";
describe("assertTaskStatusTransition", () => {
it.each([
["TODO", "IN_PROGRESS"],
["TODO", "CANCELED"],
["IN_PROGRESS", "DONE"],
["DONE", "IN_PROGRESS"],
] as const)("allows %s -> %s", (from, to) => {
expect(() => assertTaskStatusTransition(from, to)).not.toThrow();
});
it("rejects DONE -> TODO", () => {
expect(() => assertTaskStatusTransition("DONE", "TODO"))
.toThrow(TaskStatusTransitionNotAllowed);
});
});reference/TaskFlow_Testing_Snapshot/apps/api/src/tasks/task-status.policy.ts
export type TaskStatus = "TODO" | "IN_PROGRESS" | "DONE" | "CANCELED";
const allowed: Record<TaskStatus, readonly TaskStatus[]> = {
TODO: ["IN_PROGRESS", "CANCELED"],
IN_PROGRESS: ["TODO", "DONE", "CANCELED"],
DONE: ["IN_PROGRESS"],
CANCELED: [],
};
export class TaskStatusTransitionNotAllowed extends Error {}
export function assertTaskStatusTransition(from: TaskStatus, to: TaskStatus) {
if (!allowed[from].includes(to)) {
throw new TaskStatusTransitionNotAllowed(`${from} -> ${to}`);
}
}reference/TaskFlow_Testing_Snapshot/apps/api/test/fake-task.repository.ts
import type { TaskRepository, TaskRecord } from "../src/tasks/repositories/task.repository";
export class FakeTaskRepository implements TaskRepository {
readonly items = new Map<string, TaskRecord>();
seed(task: TaskRecord) { this.items.set(task.id, structuredClone(task)); }
async findById(id: string) { return this.items.get(id) ?? null; }
// Implement the rest of the contract with the minimum behavior each service test needs.
}reference/TaskFlow_Testing_Snapshot/apps/api/test/prisma-task.repository.integration.spec.ts
import { beforeEach, describe, expect, it } from "vitest";
// Reference skeleton: point PrismaService at TEST_DATABASE_URL only.
// Apply migrations before this suite, reset rows between tests, and seed two workspaces.
describe("PrismaTaskRepository integration", () => {
beforeEach(async () => {
// resetTestDatabase(); seedScenario();
});
it("never returns tasks outside the caller workspace scope", async () => {
// const result = await repository.listScoped(USER_A, query);
// expect(result.data.map(x => x.id)).toEqual([TASK_A]);
expect(true).toBe(true);
});
});reference/TaskFlow_Testing_Snapshot/apps/api/test/tasks.e2e-spec.ts
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { Test } from "@nestjs/testing";
import request from "supertest";
import { AppModule } from "../src/app.module";
import { SupabaseAuthService } from "../src/auth/supabase-auth.service";
describe("Tasks HTTP", () => {
let app: any;
beforeAll(async () => {
const mod = await Test.createTestingModule({ imports: [AppModule] })
.overrideProvider(SupabaseAuthService)
.useValue({ getClaims: async (token: string) => token === "member-token" ? { sub: "member-sub" } : null })
.compile();
app = mod.createNestApplication();
await app.init();
});
afterAll(async () => app?.close());
it("rejects a missing bearer token", async () => {
await request(app.getHttpServer()).get("/tasks").expect(401);reference/TaskFlow_Testing_Snapshot/apps/api/test/tasks.service.spec.ts
import { beforeEach, describe, expect, it } from "vitest";
import { Test } from "@nestjs/testing";
import { TasksService } from "../src/tasks/tasks.service";
import { TASK_REPOSITORY } from "../src/tasks/repositories/task.repository";
import { FakeTaskRepository } from "./fake-task.repository";
describe("TasksService", () => {
let service: TasksService;
let repo: FakeTaskRepository;
beforeEach(async () => {
repo = new FakeTaskRepository();
const moduleRef = await Test.createTestingModule({
providers: [TasksService, { provide: TASK_REPOSITORY, useValue: repo }],
}).compile();
service = moduleRef.get(TasksService);
});
it("is tested through business outcomes, not Prisma calls", async () => {
expect(service).toBeDefined();reference/TaskFlow_Testing_Snapshot/apps/web/src/components/tasks/task-form.spec.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
describe("TaskForm", () => {
it("shows an accessible title error before sending invalid input", async () => {
const user = userEvent.setup();
// render(<TaskForm ... />);
// await user.click(screen.getByRole("button", { name: /create task/i }));
// expect(await screen.findByText(/title is required/i)).toBeVisible();
expect(user).toBeDefined();
});
});reference/TaskFlow_Testing_Snapshot/apps/web/src/test/create-test-query-client.ts
import { QueryClient } from "@tanstack/react-query";
export function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
}reference/TaskFlow_Testing_Snapshot/apps/web/vitest.config.mts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
environment: "jsdom",
setupFiles: ["./vitest.setup.ts"],
},
});reference/TaskFlow_Testing_Snapshot/apps/web/vitest.setup.ts
import "@testing-library/jest-dom/vitest";
reference/TaskFlow_Testing_Snapshot/e2e/playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
use: {
baseURL: "http://127.0.0.1:3000",
trace: "on-first-retry",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
],
webServer: {
command: "pnpm --filter web start",
url: "http://127.0.0.1:3000",
reuseExistingServer: !process.env.CI,
},
});reference/TaskFlow_Testing_Snapshot/e2e/tests/task-critical-flow.spec.ts
import { test, expect } from "@playwright/test";
test("member creates a task and persisted state survives reload", async ({ page }) => {
// Reuse a test-auth storageState fixture in the real project.
await page.goto("/tasks");
await page.getByRole("button", { name: /create task/i }).click();
await page.getByLabel("Title").fill("Prepare payroll");
await page.getByRole("button", { name: /save/i }).click();
await expect(page.getByText("Prepare payroll")).toBeVisible();
await page.reload();
await expect(page.getByText("Prepare payroll")).toBeVisible();
});reference/TaskFlow_Testing_Snapshot/tests/TEST_MATRIX.md
# Phase 28 test matrix ## Unit - allowed/rejected status transitions - due-date policy with fixed time ## Service - completedAt derived correctly - business conflicts become domain exceptions ## Repository integration - tenant scope never leaks rows - filters/count/pagination share authorization scope ## HTTP - 400 validation - 401 unauthenticated - 403/404 authorization semantics - 409 business conflict - safe 500 error contract