testing-patterns
Testing Patterns and Utilities
Section titled “Testing Patterns and Utilities”Testing Philosophy
Section titled “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<X>)functions - Provide sensible defaults
- Allow overriding specific properties
- Keep tests DRY and maintainable
Test Utilities
Section titled “Test Utilities”Custom Render Function
Section titled “Custom Render Function”Create a custom render that wraps components with required providers:
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
Section titled “Factory Pattern”Component Props Factory
Section titled “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 testsit('should render with custom title', () => { const props = getMockMyComponentProps({ title: 'Custom Title' }); renderWithTheme(<MyComponent {...props} />); expect(screen.getByText('Custom Title')).toBeTruthy();});Data Factory
Section titled “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, };};
// Usageit('should display admin badge for admin users', () => { const user = getMockUser({ role: 'admin' }); renderWithTheme(<UserCard user={user} />); expect(screen.getByText('Admin')).toBeTruthy();});Mocking Patterns
Section titled “Mocking Patterns”Mocking Modules
Section titled “Mocking Modules”// Mock entire modulejest.mock('utils/analytics');
// Mock with factory functionjest.mock('utils/analytics', () => ({ Analytics: { logEvent: jest.fn(), },}));
// Access mock in testconst mockLogEvent = jest.requireMock('utils/analytics').Analytics.logEvent;Mocking GraphQL Hooks
Section titled “Mocking GraphQL Hooks”jest.mock('./GetItems.generated', () => ({ useGetItemsQuery: jest.fn(),}));
const mockUseGetItemsQuery = jest.requireMock( './GetItems.generated').useGetItemsQuery as jest.Mock;
// In testmockUseGetItemsQuery.mockReturnValue({ data: { items: [] }, loading: false, error: undefined,});Test Structure
Section titled “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
Section titled “Query Patterns”// Element must existexpect(screen.getByText('Hello')).toBeTruthy();
// Element should not existexpect(screen.queryByText('Goodbye')).toBeNull();
// Element appears asynchronouslyawait waitFor(() => { expect(screen.findByText('Loaded')).toBeTruthy();});User Interaction Patterns
Section titled “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
Section titled “Anti-Patterns to Avoid”Testing Mock Behavior Instead of Real Behavior
Section titled “Testing Mock Behavior Instead of Real Behavior”// Bad - testing the mockexpect(mockFetchData).toHaveBeenCalled();
// Good - testing actual behaviorexpect(screen.getByText('John Doe')).toBeTruthy();Not Using Factories
Section titled “Not Using Factories”// Bad - duplicated, inconsistent test datait('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 factoryconst user = getMockUser({ name: 'Custom Name' });Best Practices
Section titled “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
Section titled “Running Tests”# Run all testsnpm test
# Run with coveragenpm run test:coverage
# Run specific filenpm test ComponentName.test.tsxIntegration with Other Skills
Section titled “Integration with Other Skills”- react-ui-patterns: Test all UI states (loading, error, empty, success)
- systematic-debugging: Write test that reproduces bug before fixing
Gap Analysis Rule
Section titled “Gap Analysis Rule”Always identify gaps and suggest next steps to users. In case there is no gaps anymore, then AI should clearly state that there is no gap left.