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
| Framework | Version | Purpose | Configuration |
|---|---|---|---|
| Vitest | 4.0.17 | Unit & Integration testing | vitest.config.ts |
| Playwright | 1.57.0 | End-to-end testing | playwright.config.ts |
| Testing Library | 16.3.1 | Component testing | Global setup |
| jsdom | 27.4.0 | DOM environment | Vitest environment |
Supporting Libraries
| Library | Purpose |
|---|---|
| @testing-library/jest-dom | Custom DOM matchers |
| @testing-library/user-event | User interaction simulation |
| @vitest/coverage-v8 | Code coverage reporting |
| @vitest/ui | Visual 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.tsor*.test.tsxfor Vitest - Spec files:
*.spec.tsfor 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,getByLabelTextovergetByTestId - Test user interactions: Use
@testing-library/user-eventfor 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
waitForfor 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
waitForSelectorproperly - 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
| Metric | Target | Current |
|---|---|---|
| Line Coverage | 80% | ~65% |
| Function Coverage | 80% | ~70% |
| Branch Coverage | 80% | ~60% |
| Statement Coverage | 80% | ~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
describeblocks 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.mockfor database/network calls - Avoid over-mocking: Mock only what's necessary
- Clear mocks: Use
vi.clearAllMocks()inbeforeEach
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
waitForfor 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
waitForSelectorproperly - 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
- Vitest Documentation
- Playwright Documentation
- Testing Library
- React Testing Library
- TanStack Query Testing
Related Project Docs
- Testing Documentation - Overall testing strategy
- Architecture - Architecture overview
- Contributing - Contribution guidelines
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.