testing-patterns

Jest测试模式、工厂函数、模拟策略与TDD工作流。适用于编写单元测试、创建测试工厂或遵循TDD红-绿-重构循环时使用。

查看详情
name:testing-patternsdescription:Jest testing patterns, factory functions, mocking strategies, and TDD workflow. Use when writing unit tests, creating test factories, or following TDD red-green-refactor cycle.

Testing Patterns and Utilities

Testing Philosophy

Test-Driven Development (TDD):

  • Write failing test FIRST

  • Implement minimal code to pass

  • Refactor after green

  • Never write production code without a failing test
  • Behavior-Driven Testing:

  • Test behavior, not implementation

  • Focus on public APIs and business requirements

  • Avoid testing implementation details

  • Use descriptive test names that describe behavior
  • Factory Pattern:

  • Create getMockX(overrides?: Partial) functions

  • Provide sensible defaults

  • Allow overriding specific properties

  • Keep tests DRY and maintainable
  • Test Utilities

    Custom Render Function

    Create a custom render that wraps components with required providers:

    // src/utils/testUtils.tsx
    import { render } from '@testing-library/react-native';
    import { ThemeProvider } from './theme';

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

    Usage:

    import { renderWithTheme } from 'utils/testUtils';
    import { screen } from '@testing-library/react-native';

    it('should render component', () => {
    renderWithTheme(<MyComponent />);
    expect(screen.getByText('Hello')).toBeTruthy();
    });

    Factory Pattern

    Component Props Factory

    import { ComponentProps } from 'react';

    const getMockMyComponentProps = (
    overrides?: Partial<ComponentProps<typeof MyComponent>>
    ) => {
    return {
    title: 'Default Title',
    count: 0,
    onPress: jest.fn(),
    isLoading: false,
    ...overrides,
    };
    };

    // Usage in tests
    it('should render with custom title', () => {
    const props = getMockMyComponentProps({ title: 'Custom Title' });
    renderWithTheme(<MyComponent {...props} />);
    expect(screen.getByText('Custom Title')).toBeTruthy();
    });

    Data Factory

    interface User {
    id: string;
    name: string;
    email: string;
    role: 'admin' | 'user';
    }

    const getMockUser = (overrides?: Partial<User>): User => {
    return {
    id: '123',
    name: 'John Doe',
    email: 'john@example.com',
    role: 'user',
    ...overrides,
    };
    };

    // Usage
    it('should display admin badge for admin users', () => {
    const user = getMockUser({ role: 'admin' });
    renderWithTheme(<UserCard user={user} />);
    expect(screen.getByText('Admin')).toBeTruthy();
    });

    Mocking Patterns

    Mocking Modules

    // Mock entire module
    jest.mock('utils/analytics');

    // Mock with factory function
    jest.mock('utils/analytics', () => ({
    Analytics: {
    logEvent: jest.fn(),
    },
    }));

    // Access mock in test
    const mockLogEvent = jest.requireMock('utils/analytics').Analytics.logEvent;

    Mocking GraphQL Hooks

    jest.mock('./GetItems.generated', () => ({
    useGetItemsQuery: jest.fn(),
    }));

    const mockUseGetItemsQuery = jest.requireMock(
    './GetItems.generated'
    ).useGetItemsQuery as jest.Mock;

    // In test
    mockUseGetItemsQuery.mockReturnValue({
    data: { items: [] },
    loading: false,
    error: undefined,
    });

    Test Structure

    describe('ComponentName', () => {
    beforeEach(() => {
    jest.clearAllMocks();
    });

    describe('Rendering', () => {
    it('should render component with default props', () => {});
    it('should render loading state when loading', () => {});
    });

    describe('User interactions', () => {
    it('should call onPress when button is clicked', async () => {});
    });

    describe('Edge cases', () => {
    it('should handle empty data gracefully', () => {});
    });
    });

    Query Patterns

    // Element must exist
    expect(screen.getByText('Hello')).toBeTruthy();

    // Element should not exist
    expect(screen.queryByText('Goodbye')).toBeNull();

    // Element appears asynchronously
    await waitFor(() => {
    expect(screen.findByText('Loaded')).toBeTruthy();
    });

    User Interaction Patterns

    import { fireEvent, screen } from '@testing-library/react-native';

    it('should submit form on button click', async () => {
    const onSubmit = jest.fn();
    renderWithTheme(<LoginForm onSubmit={onSubmit} />);

    fireEvent.changeText(screen.getByLabelText('Email'), 'user@example.com');
    fireEvent.changeText(screen.getByLabelText('Password'), 'password123');
    fireEvent.press(screen.getByTestId('login-button'));

    await waitFor(() => {
    expect(onSubmit).toHaveBeenCalled();
    });
    });

    Anti-Patterns to Avoid

    Testing Mock Behavior Instead of Real Behavior

    // Bad - testing the mock
    expect(mockFetchData).toHaveBeenCalled();

    // Good - testing actual behavior
    expect(screen.getByText('John Doe')).toBeTruthy();

    Not Using Factories

    // Bad - duplicated, inconsistent test data
    it('test 1', () => {
    const user = { id: '1', name: 'John', email: 'john@test.com', role: 'user' };
    });
    it('test 2', () => {
    const user = { id: '2', name: 'Jane', email: 'jane@test.com' }; // Missing role!
    });

    // Good - reusable factory
    const user = getMockUser({ name: 'Custom Name' });

    Best Practices

  • Always use factory functions for props and data

  • Test behavior, not implementation

  • Use descriptive test names

  • Organize with describe blocks

  • Clear mocks between tests

  • Keep tests focused - one behavior per test
  • Running Tests

    # Run all tests
    npm test

    Run with coverage


    npm run test:coverage

    Run specific file


    npm test ComponentName.test.tsx

    Integration with Other Skills

  • react-ui-patterns: Test all UI states (loading, error, empty, success)

  • systematic-debugging: Write test that reproduces bug before fixing

    1. testing-patterns - Agent Skills