Course navigationPhase 28
Volume 08 · Phase 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.

16lessons + labs
4klesson words
17source references
What you will ship

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.

Figure 28.1 — Where this phase lives in TaskFlow
risk/question
choose boundary
arrange controlled state
execute behavior
assert outcome/contract
cleanup
Trace the flow left-to-right for implementation and right-to-left for debugging. At every arrow, ask what data crosses the boundary and what proves it is trustworthy.
Instructor mental model

Principles to keep while coding

01

Use unit tests for focused behavior, integration tests for collaborating infrastructure, and end-to-end tests for critical user flows.

02

Test business outcomes and contracts rather than coupling every test to private implementation details.

03

Keep tests deterministic by controlling clocks, random values, external services, and database state where necessary.

04

A useful test suite catches regressions while remaining fast enough to run continuously in development and CI.

Code-along path

Guided phase lab

Do these in order. Each step should end with evidence before the next one starts.

  1. 01

    List the highest-risk TaskFlow behaviors before choosing test counts.

  2. 02

    Write a pure unit test for one deterministic business policy.

  3. 03

    Test a service with controlled dependencies and assert business outcome rather than private calls.

  4. 04

    Run a repository integration test against real PostgreSQL with isolated data.

  5. 05

    Run an API test through NestJS for validation/auth/error contract.

  6. 06

    Write one Playwright flow for a critical user journey and make setup/cleanup reproducible.

TerminalVerification commands
pnpm test
pnpm --filter api test
pnpm --filter web test
pnpm exec playwright test

Predict what each command proves before running it. A command is useful only when its output answers a question.

Debugging studio

Failures you should know how to diagnose

Failure drill 1

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.

Failure drill 2

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.

Failure drill 3

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.

Failure drill 4

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.

Watch-order style

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.

01

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

2 min
02

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

2 min
03

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

2 min
04

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

2 min
05

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

2 min
06

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

2 min
07

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

2 min
08

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

2 min
09

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

2 min
10

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

2 min
11

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

2 min
12

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

2 min
LAB

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.

3 min
LAB

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.

2 min
LAB

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?

2 min
LAB

Mastery Capstone Lab

Integrate Phase 28 without following a transcript.

2 min
Practice ladder

Exercises that increase independence

Guided

List the highest-risk TaskFlow behaviors before choosing test counts.

Practice

Test a service with controlled dependencies and assert business outcome rather than private calls.

Debug

reproduce and diagnose — Mocking the thing the test is supposed to prove.

Independent

deliver Vitest + integration + Playwright test suite with deterministic fixtures and CI-ready commands.

Explain-back

Can choose test level by risk and Can write deterministic tests.

Mastery gate

Do not leave the phase only because you reached the bottom

Phase exit criteria

Knowledge check

Scenario and concept quiz

These questions test engineering choices rather than whether you remember a lesson title.

0/0 correct · 0/8 answered
01Which practice best matches Phase 28's engineering goal?
02Which practice best matches Phase 28's engineering goal?
03Which practice best matches Phase 28's engineering goal?
04Which practice best matches Phase 28's engineering goal?
05You are working through the Phase 28 lab. Which next action is the most disciplined step?
06You are working through the Phase 28 lab. Which next action is the most disciplined step?
07You are working through the Phase 28 lab. Which next action is the most disciplined step?
08You are working through the Phase 28 lab. Which next action is the most disciplined step?
Source snapshot

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