Skip to main content

Testing Architecture

This document provides a comprehensive overview of the testing architecture, including the testing stack, organization, patterns, and best practices used throughout the application.

Overview​

The application uses a multi-layered testing strategy designed to ensure code quality, prevent regressions, and maintain reliability across all layers of the architecture.

graph TB
subgraph "Testing Pyramid"
E2E["E2E Tests<br/>(Playwright)"]
Integration["Integration Tests<br/>(Vitest)"]
Unit["Unit Tests<br/>(Vitest)"]
end

E2E -->|"Critical User Flows"| Integration
Integration -->|"Component/API Logic"| Unit

style E2E fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px
style Integration fill:#4dabf7,stroke:#1864ab,stroke-width:2px
style Unit fill:#51cf66,stroke:#2b8a3e,stroke-width:2px

Testing Stack​

Core Frameworks​

FrameworkVersionPurposeConfiguration
Vitest4.0.17Unit & Integration testingvitest.config.ts
Playwright1.57.0End-to-end testingplaywright.config.ts
Testing Library16.3.1Component testingGlobal setup
jsdom27.4.0DOM environmentVitest environment

Supporting Libraries​

LibraryPurpose
@testing-library/jest-domCustom DOM matchers
@testing-library/user-eventUser interaction simulation
@vitest/coverage-v8Code coverage reporting
@vitest/uiVisual test runner

Why This Stack?​

  • Vitest: Native ESM support, fast execution, built-in TypeScript, Jest-compatible API
  • Playwright: Multi-browser support, mobile emulation, automatic screenshots, trace recording
  • Testing Library: Encourages testing user behavior over implementation details

Test Organization​

File Structure​

whatsapp-message-AI-analysis-agent/
├── src/
│ ├── components/ # Component tests
│ │ ├── ui/
│ │ │ ├── button.test.tsx
│ │ │ ├── input.test.tsx
│ │ │ └── ...
│ │ └── messages/
│ │ └── MultiCategoryPicker.test.tsx
│ │
│ ├── data-access/ # Data layer tests
│ │ ├── core/
│ │ │ └── users.test.ts
│ │ └── messaging/
│ │ ├── categories.test.ts
│ │ └── messages.test.ts
│ │
│ ├── fn/ # Business logic tests
│ │ └── categories.test.ts
│ │
│ ├── hooks/ # Hook tests
│ │ ├── features/
│ │ ├── messaging/
│ │ ├── subscriptions/
│ │ └── ui/
│ │
│ ├── lib/ # Library utility tests
│ │ ├── categorization/
│ │ ├── search/
│ │ └── *.test.ts
│ │
│ ├── routes/ # Route component tests
│ │ ├── dashboard.test.tsx
│ │ ├── index.test.tsx
│ │ └── ...
│ │
│ ├── server/routers/ # tRPC router tests
│ │ ├── categories.test.ts
│ │ ├── messages.test.ts
│ │ └── ...
│ │
│ ├── test/ # Test configuration
│ │ └── setup.tsx # Global setup file
│ │
│ └── utils/ # Utility tests
│ ├── __tests__/
│ ├── auth.test.ts
│ └── whatsapp-parser.test.ts
│
├── tests/ # E2E tests
│ ├── category-selection.spec.ts
│ └── cors.test.ts
│
├── vitest.config.ts # Vitest configuration
└── playwright.config.ts # Playwright configuration

Naming Conventions​

  • Test files: *.test.ts or *.test.tsx for Vitest
  • Spec files: *.spec.ts for Playwright E2E tests
  • Co-location: Test files placed next to source files

Testing Patterns by Layer​

1. Component Testing​

Purpose: Test React components in isolation

Tools: @testing-library/react, @testing-library/user-event, jsdom

Pattern: Component Testing​

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MultiCategoryPicker } from "./MultiCategoryPicker";

describe("MultiCategoryPicker", () => {
let onSelectMock: ReturnType<typeof vi.fn>;

beforeEach(() => {
onSelectMock = vi.fn();
// Mock pointer capture for Radix UI
Element.prototype.setPointerCapture = vi.fn(() => true);
Element.prototype.releasePointerCapture = vi.fn(() => true);
});

afterEach(() => {
vi.clearAllMocks();
});

describe("Rendering - Placeholder State", () => {
it("should display placeholder text when no categories are selected", () => {
render(
<MultiCategoryPicker
categories={mockCategories}
selectedCategoryIds={[]}
onSelect={onSelectMock}
/>
);

expect(screen.getByText("Select categories...")).toBeInTheDocument();
});
});

describe("Dialog Interactions", () => {
it("should open dialog when trigger button is clicked", async () => {
render(
<MultiCategoryPicker
categories={mockCategories}
selectedCategoryIds={[]}
onSelect={onSelectMock}
/>
);

const button = screen.getByRole("button");
await userEvent.click(button);

expect(screen.getByRole("dialog")).toBeInTheDocument();
});
});
});

Best Practices​

  • Test behavior, not implementation: Focus on what users see and do
  • Use semantic queries: getByRole, getByLabelText over getByTestId
  • Test user interactions: Use @testing-library/user-event for realistic interactions
  • Mock Radix UI primitives: Set up pointer capture mocks in beforeEach

2. Hook Testing​

Purpose: Test custom React hooks in isolation

Tools: @testing-library/react (renderHook), @tanstack/react-query

Pattern: Hook Testing​

import { renderHook, waitFor, act } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

// Mock dependencies
vi.mock("~/lib/auth-client", () => ({
authClient: {
useSession: vi.fn(),
},
}));

vi.mock("~/fn/conversations", () => ({
createConversationFn: vi.fn(),
updateConversationFn: vi.fn(),
}));

describe("useConversations", () => {
let queryClient: QueryClient;
let wrapper: React.FC<{ children: React.ReactNode }>;

beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});

wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
});

it("should fetch conversations", async () => {
const { result } = renderHook(() => useConversations(), { wrapper });

await waitFor(() => {
expect(result.current.data).toBeDefined();
});
});
});

Best Practices​

  • Wrap in QueryClientProvider: For React Query hooks
  • Test async states: Use waitFor for async operations
  • Mock dependencies: Isolate the hook being tested
  • Test loading/error states: Ensure proper error handling

3. Router Testing​

Purpose: Test tRPC router procedures

Tools: Vitest, tRPC caller factory

Pattern: Router Testing​

import { describe, it, expect, vi, beforeEach } from "vitest";
import { messagesRouter } from "./messages";
import { TRPCError } from "@trpc/server";

// Mock data access functions
vi.mock("~/data-access/messaging/messages", () => ({
findMessageById: vi.fn(),
findMessagesByUserId: vi.fn(),
createMessage: vi.fn(),
}));

describe("messages router", () => {
const mockUserId = "test-user-id";
const mockCtx = {
userId: mockUserId,
isAuthenticated: true,
headers: new Headers(),
};

beforeEach(() => {
vi.clearAllMocks();
vi.mocked(createMessage).mockResolvedValue(mockMessage);
});

it("should create a message", async () => {
const caller = createCallerFactory()
.context<typeof createContext>()
.create(messagesRouter);

const result = await caller.create(mockCtx, {
content: "Test message",
senderName: "John Doe",
});

expect(result).toEqual(mockMessage);
});
});

Best Practices​

  • Mock data access layer: Don't test database, test router logic
  • Test authentication: Verify protected procedures
  • Test error handling: Ensure proper error responses
  • Test input validation: Verify Zod schema validation

4. Utility Testing​

Purpose: Test pure functions and utilities

Tools: Vitest, TypeScript

Pattern: Utility Testing​

import { describe, it, expect } from "vitest";
import { parseMessages } from "./whatsapp-parser";

describe("WhatsApp Parser", () => {
describe("Message Parsing", () => {
it("should parse standard WhatsApp message format", () => {
const text = `
12/31/23, 9:41 AM - John Doe: Hello world
`;

const result = parseMessages(text, "user-123");

expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
senderName: "John Doe",
content: "Hello world",
messageType: "text",
});
});

it("should handle media messages", () => {
const text = `
12/31/23, 9:41 AM - John Doe: <image omitted>
`;

const result = parseMessages(text, "user-123");
expect(result[0].messageType).toBe("media");
});
});
});

Best Practices​

  • Test edge cases: Empty input, null values, special characters
  • Use table-driven tests: Test multiple scenarios
  • Test pure functions: No mocking needed for pure functions
  • Verify return types: Ensure type safety

5. E2E Testing​

Purpose: Test critical user flows across the application

Tools: Playwright, real browsers

Pattern: E2E Testing​

import { test, expect } from "@playwright/test";

test.describe("Category Selection", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/dashboard");
});

test("should display placeholder when no category is selected", async ({ page }) => {
const categoryPicker = page
.locator('[data-testid="message-row"]')
.filter({ hasText: "No category" })
.first()
.locator('[role="combobox"]');

await expect(categoryPicker).toBeVisible();
await expect(categoryPicker).toContainText("No category");
});

test("should select category from dropdown", async ({ page }) => {
await page.waitForSelector('[data-testid="message-row"]');

const categoryPicker = page.locator('[role="combobox"]').first();
await categoryPicker.click();

const categoryOption = page.locator('[role="option"]').first();
await categoryOption.click();

// Verify selection
await expect(categoryPicker).toContainText(categoryOption);
});
});

Best Practices​

  • Test critical paths: Focus on important user journeys
  • Use data-testid: For reliable element selection
  • Wait for elements: Use waitForSelector properly
  • Avoid flaky tests: Use stable selectors, avoid hardcoded waits

Mock Setup and Configuration​

Global Test Setup​

All tests use the global setup file: src/test/setup.tsx

import { expect, vi } from "vitest";
import * as matchers from "@testing-library/jest-dom/matchers";

// Extend Vitest's expect with jest-dom matchers
expect.extend(matchers);

// Mock environment variables
process.env.DATABASE_URL = "postgresql://test:test@localhost:5432/test";
process.env.BETTER_AUTH_SECRET = "test-secret-for-testing";
process.env.R2_ACCESS_KEY_ID = "test";
process.env.R2_SECRET_ACCESS_KEY = "test";

// Mock TanStack Router
vi.mock("@tanstack/react-router", async () => {
const actual = await vi.importActual("@tanstack/react-router");
return {
...actual,
createFileRoute: vi.fn((path: string) => ({
component: vi.fn(),
loader: vi.fn(),
beforeLoad: vi.fn(),
})),
Link: ({ children, to, ...props }: any) => <a href={to} {...props}>{children}</a>,
useNavigate: () => vi.fn(),
};
});

// Mock Radix UI primitives
Element.prototype.setPointerCapture = vi.fn(() => true);
Element.prototype.releasePointerCapture = vi.fn(() => true);

Mock Patterns​

1. Module Mocking​

// Mock entire module
vi.mock("~/lib/auth-client", () => ({
authClient: {
useSession: vi.fn(),
},
}));

// Use mock in tests
vi.mocked(authClient.useSession).mockReturnValue({
data: { user: { id: "test-id" } }
});

2. Partial Mocking​

// Mock specific functions
vi.mock("~/fn/conversations", () => ({
createConversationFn: vi.fn(),
updateConversationFn: vi.fn(),
// Keep other functions real
}));

3. Spying on Functions​

// Spy on real implementation
const spy = vi.spyOn(console, "log");
expect(spy).toHaveBeenCalledWith("test");
spy.mockRestore();

CI/CD Integration​

GitHub Actions Workflow​

name: Test Suite

on:
pull_request:
branches: [master]
push:
branches: [master]

jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm run test:coverage

- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info

e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm ci

- name: Install Playwright browsers
run: npx playwright install --with-deps

- name: Run E2E tests
run: npm run test:e2e

- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/

CI Best Practices​

  • Run tests in parallel: Separate unit and E2E tests
  • Cache dependencies: Speed up workflow execution
  • Upload coverage reports: Track coverage over time
  • Run on PRs: Catch issues before merge
  • Fail fast: Stop workflow on test failure

Coverage Reporting​

Configuration​

Coverage is configured in vitest.config.ts:

coverage: {
provider: "v8",
reporter: ["text", "json", "html", "lcov"],
exclude: [
"node_modules/",
"src/test/",
"**/*.d.ts",
"**/*.config.*",
"**/mockData/**",
"src/routes/__root.tsx",
"src/examples/**",
"docs/**",
"temp/**",
"website/**",
],
all: true,
lines: 80, // Target: 80%
functions: 80, // Target: 80%
branches: 80, // Target: 80%
statements: 80, // Target: 80%
}

Generating Coverage​

# Generate coverage report
npm run test:coverage

# View HTML coverage report
open coverage/index.html

# CI Integration
npm run test:coverage -- --reporter=json --reporter=lcov

Coverage Goals​

MetricTargetCurrent
Line Coverage80%~65%
Function Coverage80%~70%
Branch Coverage80%~60%
Statement Coverage80%~65%

Coverage Tools​

  • Local: npm run test:coverage - Generate HTML report
  • CI: Codecov or Coveralls for tracking
  • PR Comments: Automated coverage comments on PRs
  • Badges: Display coverage in README

Test Execution​

Vitest Commands​

# Watch mode (interactive)
npm run test

# Run tests once (CI mode)
npm run test:run

# Run tests with UI
npm run test:ui

# Run tests with coverage
npm run test:coverage

# Watch mode for specific file
npm run test -- MultiCategoryPicker

# Run tests matching pattern
npm run test -- --grep "should create"

Playwright Commands​

# Run E2E tests
npm run test:e2e

# Run E2E tests with UI
npm run test:e2e:ui

# Run E2E tests in headed mode (visible browser)
npm run test:e2e:headed

# Run E2E tests for specific file
npx playwright test category-selection

# Run specific browser
npx playwright test --project=chromium

# Debug E2E tests
npx playwright test --debug

Testing Best Practices​

1. Test Organization​

  • Co-location: Keep test files next to source files
  • Descriptive names: Use describe blocks to group related tests
  • One assertion per test: Keep tests focused and small
  • AAA pattern: Arrange, Act, Assert structure
describe("Feature", () => {
it("should do something when condition is met", () => {
// Arrange - Setup test data
const input = { value: "test" };

// Act - Execute the function
const result = functionUnderTest(input);

// Assert - Verify the result
expect(result).toBe("expected");
});
});

2. Mocking Strategy​

  • Mock external dependencies: Don't test third-party libraries
  • Mock data layer: Use vi.mock for database/network calls
  • Avoid over-mocking: Mock only what's necessary
  • Clear mocks: Use vi.clearAllMocks() in beforeEach
beforeEach(() => {
vi.clearAllMocks();
// Setup fresh mocks for each test
});

3. Component Testing​

  • Test user interactions: Use @testing-library/user-event
  • Test behavior, not implementation: Focus on what users see/do
  • Use data-testid: For elements without semantic meaning
  • Test edge cases: Empty states, loading, error states
test("should show error message when save fails", async () => {
vi.mocked(saveMessage).mockRejectedValue(new Error("Save failed"));

render(<MessageForm />);

await userEvent.click(screen.getByRole("button", { name: /save/i }));

await waitFor(() => {
expect(screen.getByText("Save failed")).toBeInTheDocument();
});
});

4. Hook Testing​

  • Use renderHook: From @testing-library/react
  • Wrap in QueryClient: For React Query hooks
  • Test async states: Use waitFor for async operations
  • Mock dependencies: Isolate the hook being tested

5. E2E Testing​

  • Test critical paths: Focus on important user journeys
  • Use data-testid: For reliable element selection
  • Wait for elements: Use waitForSelector properly
  • Avoid flaky tests: Use stable selectors, avoid hardcoded waits
test("should complete user flow", async ({ page }) => {
// Navigate
await page.goto("/dashboard");

// Wait for element
await page.waitForSelector('[data-testid="message-list"]');

// Interact
await page.click('[data-testid="add-message"]');

// Assert
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
});

6. Common Anti-Patterns to Avoid​

  • ❌ Testing implementation details instead of behavior
  • ❌ Over-mocking leading to false confidence
  • ❌ Flaky tests due to timing issues
  • ❌ Testing third-party libraries
  • ❌ Not testing error states
  • ❌ Hardcoded values and magic numbers
  • ❌ Testing private methods directly

Test Metrics and Monitoring​

Key Metrics​

  • Test Count: Track total number of tests
  • Pass Rate: Monitor percentage of passing tests
  • Execution Time: Track test suite performance
  • Coverage Trends: Monitor coverage over time
  • Flaky Tests: Identify and fix unreliable tests

Test Health Dashboard​

Consider setting up:

  • Test history: Track test results over time
  • Flaky test detection: Identify tests that intermittently fail
  • Performance trends: Monitor test execution time
  • Coverage badges: Display coverage in README/PRs

Troubleshooting​

Common Issues​

1. "No test suite found" Error​

Solution: Ensure test file has proper describe blocks

// Correct
describe("Feature", () => {
it("should work", () => {
expect(true).toBe(true);
});
});

// Incorrect - No describe block
it("should work", () => {
expect(true).toBe(true);
});

2. TanStack Router Mocking Issues​

Problem: createFileRoute is not a function

Solution: Already configured in src/test/setup.tsx

3. jsdom Issues with Radix UI​

Problem: setPointerCapture not defined

Solution: Mock in beforeEach:

beforeEach(() => {
Element.prototype.setPointerCapture = vi.fn(() => true);
Element.prototype.releasePointerCapture = vi.fn(() => true);
});

4. Environment Variables Missing​

Solution: Mock in src/test/setup.tsx:

process.env.DATABASE_URL = "postgresql://test:test@localhost:5432/test";
process.env.BETTER_AUTH_SECRET = "test-secret-for-testing";

5. Playwright Tests Timeout​

Solution: Start dev server manually or configure webServer in playwright.config.ts

Resources​

Documentation​

Summary​

This testing architecture provides a comprehensive strategy for maintaining code quality across all layers of the application:

  • Unit tests: Fast, isolated tests for utilities and functions
  • Integration tests: Component and hook tests with mocked dependencies
  • E2E tests: Critical user flows tested in real browsers
  • High coverage target: 80% across all metrics
  • CI/CD integration: Automated testing on every PR
  • Clear patterns: Consistent testing patterns across the codebase

By following this architecture and the patterns outlined in this document, developers can ensure high-quality, reliable code that is easy to maintain and refactor.