Skip to content

Testing Guide

Comprehensive testing strategies for the Petfolio monorepo.

Testing Philosophy

We follow the testing pyramid approach:

graph TD
    A[E2E Tests<br/>Few, High Value] --> B[Integration Tests<br/>Moderate Coverage]
    B --> C[Unit Tests<br/>Comprehensive Coverage]

    style A fill:#e57373
    style B fill:#ffb74d
    style C fill:#81c784
  • Unit Tests: Test individual components and functions in isolation
  • Integration Tests: Test how components work together
  • E2E Tests: Test complete user workflows

Unit Testing with Jest

Running Tests

# Run all tests
npx nx run-many --target=test --all

# Test specific project
npx nx test ui-component-library

# Watch mode
npx nx test ui-component-library --watch

# Coverage report
npx nx test ui-component-library --coverage

Test Structure

Button.spec.tsx
import { render, screen } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('renders with correct text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);

    screen.getByText('Click me').click();

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('renders with variant styles', () => {
    render(<Button variant="primary">Primary</Button>);
    const button = screen.getByText('Primary');
    expect(button).toHaveClass('variant-primary');
  });
});

Testing Hooks

import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
    it('increments counter', () => {
        const { result } = renderHook(() => useCounter());

        act(() => {
            result.current.increment();
        });

        expect(result.current.count).toBe(1);
    });
});

Mocking

// Mock external dependencies
jest.mock('@util-global-theme', () => ({
    useTheme: () => ({ palette: { primary: { main: '#000' } } }),
}));

// Mock API calls
jest.mock('./api', () => ({
    fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'Test' }),
}));

E2E Testing with Playwright

Running E2E Tests

# Run all e2e tests
npx nx e2e petfolio-ui-tests

# Run specific test
npx nx e2e petfolio-ui-tests --spec=login.spec.ts

# Run in UI mode
npx nx e2e petfolio-ui-tests --ui

# Debug mode
npx nx e2e petfolio-ui-tests --debug

Test Structure

login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Login Flow', () => {
    test('should login successfully with valid credentials', async ({
        page,
    }) => {
        await page.goto('/login');

        await page.fill('[name="email"]', 'user@example.com');
        await page.fill('[name="password"]', 'password123');
        await page.click('button[type="submit"]');

        await expect(page).toHaveURL('/dashboard');
        await expect(page.locator('h1')).toContainText('Dashboard');
    });

    test('should show error with invalid credentials', async ({ page }) => {
        await page.goto('/login');

        await page.fill('[name="email"]', 'invalid@example.com');
        await page.fill('[name="password"]', 'wrong');
        await page.click('button[type="submit"]');

        await expect(page.locator('.error-message')).toBeVisible();
    });
});

Best Practices

!!! tip "Use Data Attributes" Use data-testid for reliable selectors: tsx <button data-testid="submit-button">Submit</button> typescript await page.click('[data-testid="submit-button"]');

!!! tip "Wait for Navigation" Wait for navigation to complete: typescript await Promise.all([ page.waitForNavigation(), page.click('button[type="submit"]'), ]);

Visual Testing with Storybook

Creating Stories

Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
    title: 'Atoms/Button',
    component: Button,
    parameters: {
        layout: 'centered',
    },
    tags: ['autodocs'],
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
    args: {
        variant: 'primary',
        children: 'Primary Button',
    },
};

export const Secondary: Story = {
    args: {
        variant: 'secondary',
        children: 'Secondary Button',
    },
};

Running Storybook

# Start Storybook
npx nx storybook ui-component-library

# Build static Storybook
npx nx build-storybook ui-component-library

Testing Patterns

Arrange-Act-Assert

test('should update user profile', async () => {
    // Arrange
    const user = { id: 1, name: 'John' };
    const updateData = { name: 'Jane' };

    // Act
    const result = await updateUserProfile(user.id, updateData);

    // Assert
    expect(result.name).toBe('Jane');
});

Given-When-Then

test('should display error when form is invalid', () => {
  // Given a form with validation rules
  render(<ContactForm />);

  // When submitting with empty fields
  screen.getByRole('button', { name: /submit/i }).click();

  // Then error messages should appear
  expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});

Coverage Goals

Type Target Current
Unit Tests 80%+ TBD
Integration 60%+ TBD
E2E Critical Paths 100% TBD

View coverage reports:

npx nx test ui-component-library --coverage
open coverage/index.html

CI Testing

Tests run automatically in CI:

sequenceDiagram
    participant Dev as Developer
    participant GH as GitHub
    participant CI as CI Pipeline
    participant Report as Test Report

    Dev->>GH: Push commit
    GH->>CI: Trigger workflow
    CI->>CI: Run unit tests
    CI->>CI: Run e2e tests
    CI->>Report: Generate coverage
    Report-->>Dev: Show results in PR

Accessibility Testing

Storybook A11y Addon

Storybook includes accessibility testing:

  1. Open Storybook
  2. Select a story
  3. Click "Accessibility" tab
  4. Review violations

Manual A11y Testing

import { axe } from 'jest-axe';

test('should have no accessibility violations', async () => {
  const { container } = render(<Button>Click me</Button>);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Performance Testing

Component Performance

import { render } from '@testing-library/react';

test('should render quickly', () => {
  const start = performance.now();
  render(<ExpensiveComponent data={largeDataset} />);
  const end = performance.now();

  expect(end - start).toBeLessThan(100); // Should render in < 100ms
});

Debugging Tests

Debug in VS Code

  1. Set breakpoint in test
  2. Click "Debug" in test file
  3. Step through code

Playwright Debug

# Open Playwright Inspector
npx nx e2e petfolio-ui-tests --debug

# Generate trace
npx nx e2e petfolio-ui-tests --trace on

Common Testing Issues

Async Issues

// ❌ Bad - doesn't wait
test('loads data', () => {
  render(<AsyncComponent />);
  expect(screen.getByText('Data loaded')).toBeInTheDocument();
});

// ✅ Good - waits for element
test('loads data', async () => {
  render(<AsyncComponent />);
  await screen.findByText('Data loaded');
});

Theme Provider Issues

// Wrap components that use theme
import { ThemeProvider } from '@util-global-theme';

const renderWithTheme = (component: React.ReactElement) => {
  return render(<ThemeProvider>{component}</ThemeProvider>);
};

test('renders with theme', () => {
  renderWithTheme(<ThemedButton />);
});

Next Steps