UTA DevHub
UI Development/UI Architecture/Component Development Workflow

Testing Strategies

Write unit tests, visual tests, accessibility tests, and integration tests for your components.

Testing Strategies

Overview

Testing is an essential part of the component development process. A well-tested component is more reliable, easier to maintain, and less likely to cause issues when used in different contexts. This guide covers comprehensive testing strategies for React Native components.

Why Testing Matters

Testing is an essential part of the component development process. A well-tested component is more reliable, easier to maintain, and less likely to cause issues when used in different contexts.

Step 8: Implement Component Tests

Our testing approach ensures component quality through multiple levels of verification, from unit tests to accessibility compliance.

Our testing approach includes multiple levels to ensure component quality:

  1. Unit Testing

    • Test individual functions and methods
    • Verify prop handling and default values
    • Test component state management
  2. Component Testing

    • Test component rendering
    • Verify component interactions
    • Test different prop combinations
  3. Integration Testing

    • Test interaction with other components
    • Verify context and provider integration
    • Test real-world usage scenarios
  4. Accessibility Testing

    • Verify screen reader compatibility
    • Test keyboard navigation
    • Check WCAG compliance
  5. Visual Testing

    • Snapshot testing for UI regressions
    • Platform-specific rendering verification
    • Theme and style verification

Types of Component Tests

1. Rendering Tests

Verify that components render correctly with different prop combinations:

// ui/foundation/Badge/Badge.test.tsx
import React from 'react';
import { render } from '@testing-library/react-native';
import { Badge } from './Badge';
 
describe('Badge Rendering', () => {
  it('renders with default props', () => {
    const { getByText } = render(<Badge>New</Badge>);
    expect(getByText('New')).toBeTruthy();
  });
 
  it('renders all variants correctly', () => {
    const variants = ['primary', 'success', 'warning', 'error'] as const;
    
    variants.forEach(variant => {
      const { getByTestId } = render(
        <Badge variant={variant} testID={`badge-${variant}`}>
          Test
        </Badge>
      );
      
      const badge = getByTestId(`badge-${variant}`);
      expect(badge).toBeTruthy();
      // Verify variant-specific styles are applied
    });
  });
 
  it('renders with custom props', () => {
    const { getByTestId } = render(
      <Badge 
        size="large" 
        rounded 
        testID="custom-badge"
      >
        Custom
      </Badge>
    );
    
    const badge = getByTestId('custom-badge');
    expect(badge).toBeTruthy();
  });
});

2. Interaction Tests

Test user interactions and event handling:

// ui/patterns/Accordion/Accordion.test.tsx
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { Accordion } from './Accordion';
 
describe('Accordion Interactions', () => {
  const mockItems = [
    { title: 'Section 1', content: 'Content 1' },
    { title: 'Section 2', content: 'Content 2' },
  ];
 
  it('expands and collapses sections on press', async () => {
    const { getByText, queryByText } = render(
      <Accordion items={mockItems} />
    );
    
    // Initially collapsed
    expect(queryByText('Content 1')).toBeNull();
    
    // Expand section
    fireEvent.press(getByText('Section 1'));
    await waitFor(() => {
      expect(getByText('Content 1')).toBeTruthy();
    });
    
    // Collapse section
    fireEvent.press(getByText('Section 1'));
    await waitFor(() => {
      expect(queryByText('Content 1')).toBeNull();
    });
  });
 
  it('handles multiple sections correctly', () => {
    const { getByText } = render(
      <Accordion items={mockItems} allowMultiple />
    );
    
    // Expand both sections
    fireEvent.press(getByText('Section 1'));
    fireEvent.press(getByText('Section 2'));
    
    // Both should be visible
    expect(getByText('Content 1')).toBeTruthy();
    expect(getByText('Content 2')).toBeTruthy();
  });
});

3. State Management Tests

Test component state and lifecycle:

// ui/patterns/Form/Form.test.tsx
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react-native';
import { Form, FormField } from './Form';
 
describe('Form State Management', () => {
  it('manages form values correctly', () => {
    const onSubmit = jest.fn();
    const { getByPlaceholderText, getByText } = render(
      <Form onSubmit={onSubmit}>
        <FormField 
          name="email" 
          placeholder="Email"
        />
        <FormField 
          name="password" 
          placeholder="Password"
          secureTextEntry
        />
        <Button onPress={() => {}}>Submit</Button>
      </Form>
    );
    
    // Update form fields
    fireEvent.changeText(
      getByPlaceholderText('Email'), 
      'test@example.com'
    );
    fireEvent.changeText(
      getByPlaceholderText('Password'), 
      'password123'
    );
    
    // Submit form
    fireEvent.press(getByText('Submit'));
    
    expect(onSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123'
    });
  });
 
  it('validates form fields', async () => {
    const validate = (values: any) => {
      const errors: any = {};
      if (!values.email) errors.email = 'Email is required';
      return errors;
    };
    
    const { getByText, getByTestId } = render(
      <Form validate={validate}>
        <FormField 
          name="email" 
          testID="email-field"
        />
        <Button onPress={() => {}}>Submit</Button>
      </Form>
    );
    
    // Try to submit without filling required field
    fireEvent.press(getByText('Submit'));
    
    await waitFor(() => {
      expect(getByText('Email is required')).toBeTruthy();
    });
  });
});

4. Accessibility Tests

Ensure components are accessible to all users:

// ui/foundation/Button/Button.accessibility.test.tsx
import React from 'react';
import { render } from '@testing-library/react-native';
import { Button } from './Button';
 
describe('Button Accessibility', () => {
  it('has correct accessibility role', () => {
    const { getByRole } = render(<Button>Press me</Button>);
    const button = getByRole('button');
    expect(button).toBeTruthy();
  });
  
  it('has correct accessibility label', () => {
    const { getByLabelText } = render(
      <Button accessibilityLabel="Save changes">Save</Button>
    );
    expect(getByLabelText('Save changes')).toBeTruthy();
  });
  
  it('uses button text as label when no accessibilityLabel provided', () => {
    const { getByLabelText } = render(<Button>Click here</Button>);
    expect(getByLabelText('Click here')).toBeTruthy();
  });
  
  it('communicates disabled state', () => {
    const { getByRole } = render(
      <Button disabled>Press me</Button>
    );
    const button = getByRole('button');
    expect(button.props.accessibilityState.disabled).toBe(true);
  });
  
  it('communicates loading state', () => {
    const { getByRole } = render(
      <Button loading>Press me</Button>
    );
    const button = getByRole('button');
    expect(button.props.accessibilityState.busy).toBe(true);
  });
 
  it('has adequate touch target size', () => {
    const { getByRole } = render(<Button>Tap</Button>);
    const button = getByRole('button');
    const { height, width } = button.props.style;
    
    // Minimum 44x44 points for touch targets
    expect(height).toBeGreaterThanOrEqual(44);
    expect(width).toBeGreaterThanOrEqual(44);
  });
});

5. Visual Regression Tests

Catch unintended visual changes:

// ui/foundation/Card/Card.visual.test.tsx
import React from 'react';
import { render } from '@testing-library/react-native';
import { Card } from './Card';
 
describe('Card Visual Regression', () => {
  const variants = ['default', 'elevated', 'outlined'] as const;
  const sizes = ['small', 'medium', 'large'] as const;
 
  variants.forEach(variant => {
    sizes.forEach(size => {
      it(`matches snapshot for ${variant} variant with ${size} size`, () => {
        const { toJSON } = render(
          <Card variant={variant} size={size}>
            <Text>Card Content</Text>
            <Text>Additional content for testing</Text>
          </Card>
        );
        
        expect(toJSON()).toMatchSnapshot();
      });
    });
  });
 
  it('matches snapshot with all props', () => {
    const { toJSON } = render(
      <Card
        variant="elevated"
        size="large"
        padding="medium"
        onPress={() => {}}
      >
        <Text>Interactive Card</Text>
      </Card>
    );
    
    expect(toJSON()).toMatchSnapshot();
  });
});

6. Performance Tests

Ensure components perform well:

// ui/patterns/VirtualizedList/VirtualizedList.performance.test.tsx
import React from 'react';
import { render, measure } from '@testing-library/react-native';
import { VirtualizedList } from './VirtualizedList';
 
describe('VirtualizedList Performance', () => {
  const generateItems = (count: number) => 
    Array.from({ length: count }, (_, i) => ({
      id: `item-${i}`,
      title: `Item ${i}`,
    }));
 
  it('renders large lists efficiently', async () => {
    const items = generateItems(1000);
    
    const renderTime = await measure(() => {
      render(
        <VirtualizedList
          data={items}
          renderItem={({ item }) => <Text>{item.title}</Text>}
          keyExtractor={item => item.id}
        />
      );
    });
    
    // Should render in under 100ms
    expect(renderTime).toBeLessThan(100);
  });
 
  it('handles rapid scrolling without lag', async () => {
    const items = generateItems(5000);
    const { getByTestId } = render(
      <VirtualizedList
        testID="list"
        data={items}
        renderItem={({ item }) => <Text>{item.title}</Text>}
        keyExtractor={item => item.id}
      />
    );
    
    const list = getByTestId('list');
    
    // Simulate rapid scrolling
    const scrollPerformance = await measure(() => {
      for (let i = 0; i < 10; i++) {
        fireEvent.scroll(list, {
          nativeEvent: {
            contentOffset: { y: i * 1000 },
          },
        });
      }
    });
    
    // Should handle scrolling smoothly
    expect(scrollPerformance).toBeLessThan(50);
  });
});

Test Organization

Test File Structure

Organize tests alongside components for easy maintenance:

ui/foundation/Button/
├── Button.tsx
├── Button.test.tsx              # Main component tests
├── Button.accessibility.test.tsx # Accessibility-specific tests
├── Button.visual.test.tsx       # Visual regression tests
├── Button.performance.test.tsx  # Performance tests (if needed)
└── __snapshots__/              # Jest snapshots

Test Utilities

Create shared test utilities for common scenarios:

// ui/test-utils/render.tsx
import React from 'react';
import { render as rtlRender } from '@testing-library/react-native';
import { ThemeProvider } from '@/core/shared/contexts/ThemeContext';
 
export function render(
  ui: React.ReactElement,
  options?: any
) {
  const Wrapper = ({ children }: { children: React.ReactNode }) => (
    <ThemeProvider>
      {children}
    </ThemeProvider>
  );
 
  return rtlRender(ui, { wrapper: Wrapper, ...options });
}
 
// Re-export everything
export * from '@testing-library/react-native';

Mock Utilities

Create consistent mocks for common dependencies:

// ui/test-utils/mocks.ts
export const mockNavigation = {
  navigate: jest.fn(),
  goBack: jest.fn(),
  setOptions: jest.fn(),
};
 
export const mockTheme = {
  colors: {
    primary: { 
      500: '#3b82f6',
      600: '#2563eb',
    },
    // ... other colors
  },
  spacing: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
  },
  // ... other theme properties
};

AI Collaboration: Test Implementation

Use AI to help generate comprehensive test suites:

# Component Test Implementation
 
Please help me create tests for a [ComponentName] component:
 
## Component Information
- Component: [ComponentName]
- Category: [foundation/pattern/business]
- Key Props: [list main props]
- User Interactions: [describe key interactions]
 
## Testing Requirements
- Unit tests for props and rendering
- Interaction tests for user actions
- Snapshot tests for UI verification
- Accessibility tests
- Edge case tests
 
## Implementation Needs
- Set up test boilerplate with proper imports
- Create comprehensive test cases
- Implement proper mocks for dependencies
- Show how to test each key component feature
- Include examples for async behavior testing if needed
 
Please provide a complete test implementation for this component.

Testing Best Practices Summary

(Do ✅) Testing Best Practices

  • Write tests alongside implementation
  • Test from the user's perspective
  • Cover all component states and variations
  • Include accessibility in your test suite
  • Use descriptive test names that explain the scenario
  • Mock external dependencies appropriately
  • Keep tests focused and isolated
  • Test error scenarios and edge cases

Next Steps

After completing your component tests:

  1. Run the Full Test Suite: Ensure all tests pass consistently
  2. Check Coverage: Verify you meet coverage requirements
  3. Review Test Quality: Ensure tests are meaningful and maintainable
  4. Move to Review Phase: Proceed to the Component Review Process phase