component-testing-mobile

IvanTorresEdge's avatarfrom IvanTorresEdge

Jest and React Native Testing Library patterns. Use when writing component tests.

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 1, 2026

When & Why to Use This Skill

This Claude skill provides a comprehensive set of patterns and best practices for testing React Native components using Jest and React Native Testing Library (RNTL). It focuses on behavior-driven testing to ensure mobile applications are robust, accessible, and maintainable by covering everything from basic rendering to complex asynchronous workflows and native module mocking.

Use Cases

  • Unit Testing Components: Verifying that React Native components render correctly and respond as expected to user interactions like presses and text changes.
  • Async Operation Validation: Using waitFor and act utilities to test components that fetch data from APIs or perform background tasks.
  • Native Module Mocking: Setting up mock environments for platform-specific modules such as Expo SecureStore, AsyncStorage, and navigation libraries.
  • Accessibility Testing: Ensuring mobile UI elements are accessible by testing with ARIA roles, labels, and accessibility states.
  • Custom Hook & State Management Testing: Validating the logic of custom React hooks and global state stores like Zustand in isolation.
  • Form & List Validation: Testing complex user input flows, validation error messages, and the rendering of dynamic lists.
namecomponent-testing-mobile
descriptionJest and React Native Testing Library patterns. Use when writing component tests.

Component Testing Mobile Skill

This skill covers testing React Native components with Jest and RNTL.

When to Use

Use this skill when:

  • Writing unit tests for components
  • Testing hooks and utilities
  • Testing component interactions
  • Mocking native modules

Core Principle

TEST BEHAVIOR - Test what users see and do, not implementation details.

Installation

npm install --save-dev @testing-library/react-native jest @types/jest

Jest Configuration

// jest.config.js
module.exports = {
  preset: 'jest-expo',
  setupFilesAfterEnv: ['@testing-library/react-native/extend-expect'],
  transformIgnorePatterns: [
    'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)',
  ],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
  },
  collectCoverageFrom: [
    '**/*.{ts,tsx}',
    '!**/node_modules/**',
    '!**/coverage/**',
    '!**/*.d.ts',
  ],
};

Basic Component Test

// components/__tests__/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react-native';
import { Button } from '../Button';

describe('Button', () => {
  it('renders with text', () => {
    render(<Button>Press me</Button>);

    expect(screen.getByText('Press me')).toBeOnTheScreen();
  });

  it('calls onPress when pressed', () => {
    const onPress = jest.fn();
    render(<Button onPress={onPress}>Press me</Button>);

    fireEvent.press(screen.getByText('Press me'));

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

  it('is disabled when disabled prop is true', () => {
    const onPress = jest.fn();
    render(<Button onPress={onPress} disabled>Press me</Button>);

    fireEvent.press(screen.getByText('Press me'));

    expect(onPress).not.toHaveBeenCalled();
  });
});

Testing with Accessibility

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

describe('AccessibleButton', () => {
  it('has correct accessibility role', () => {
    render(<Button accessibilityRole="button">Submit</Button>);

    expect(screen.getByRole('button')).toBeOnTheScreen();
  });

  it('has accessibility label', () => {
    render(
      <Button accessibilityLabel="Submit form">
        <Icon name="check" />
      </Button>
    );

    expect(screen.getByLabelText('Submit form')).toBeOnTheScreen();
  });
});

Testing Async Operations

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

describe('UserProfile', () => {
  it('shows loading state initially', () => {
    render(<UserProfile userId="123" />);

    expect(screen.getByText('Loading...')).toBeOnTheScreen();
  });

  it('shows user data after loading', async () => {
    render(<UserProfile userId="123" />);

    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeOnTheScreen();
    });
  });

  it('shows error on fetch failure', async () => {
    server.use(
      rest.get('/api/users/123', (req, res, ctx) => {
        return res(ctx.status(500));
      })
    );

    render(<UserProfile userId="123" />);

    await waitFor(() => {
      expect(screen.getByText('Error loading user')).toBeOnTheScreen();
    });
  });
});

Testing Forms

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

describe('LoginForm', () => {
  it('shows validation errors for empty submission', async () => {
    render(<LoginForm />);

    fireEvent.press(screen.getByText('Sign In'));

    await waitFor(() => {
      expect(screen.getByText('Email is required')).toBeOnTheScreen();
    });
  });

  it('submits with valid data', async () => {
    const onSubmit = jest.fn();
    render(<LoginForm onSubmit={onSubmit} />);

    fireEvent.changeText(
      screen.getByPlaceholderText('Email'),
      'test@example.com'
    );
    fireEvent.changeText(
      screen.getByPlaceholderText('Password'),
      'password123'
    );
    fireEvent.press(screen.getByText('Sign In'));

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        email: 'test@example.com',
        password: 'password123',
      });
    });
  });
});

Testing Lists

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

describe('TodoList', () => {
  const items = [
    { id: '1', text: 'Buy groceries' },
    { id: '2', text: 'Walk the dog' },
  ];

  it('renders all items', () => {
    render(<TodoList items={items} />);

    expect(screen.getByText('Buy groceries')).toBeOnTheScreen();
    expect(screen.getByText('Walk the dog')).toBeOnTheScreen();
  });

  it('calls onItemPress with correct item', () => {
    const onItemPress = jest.fn();
    render(<TodoList items={items} onItemPress={onItemPress} />);

    fireEvent.press(screen.getByText('Buy groceries'));

    expect(onItemPress).toHaveBeenCalledWith(items[0]);
  });
});

Mocking Native Modules

// jest.setup.js
jest.mock('expo-secure-store', () => ({
  getItemAsync: jest.fn(),
  setItemAsync: jest.fn(),
  deleteItemAsync: jest.fn(),
}));

jest.mock('expo-router', () => ({
  useRouter: () => ({
    push: jest.fn(),
    replace: jest.fn(),
    back: jest.fn(),
  }),
  useLocalSearchParams: () => ({}),
}));

jest.mock('@react-native-async-storage/async-storage', () =>
  require('@react-native-async-storage/async-storage/jest/async-storage-mock')
);

Testing Hooks

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

describe('useCounter', () => {
  it('starts with initial value', () => {
    const { result } = renderHook(() => useCounter(10));

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

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

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

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

Testing with Providers

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render } from '@testing-library/react-native';

function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
    },
  });

  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

describe('UserList', () => {
  it('fetches and displays users', async () => {
    render(<UserList />, { wrapper: createWrapper() });

    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeOnTheScreen();
    });
  });
});

Testing Zustand Stores

import { useAuthStore } from '../authStore';

describe('authStore', () => {
  beforeEach(() => {
    useAuthStore.setState({
      user: null,
      token: null,
      isAuthenticated: false,
    });
  });

  it('sets user on login', async () => {
    await useAuthStore.getState().login('test@test.com', 'password');

    expect(useAuthStore.getState().isAuthenticated).toBe(true);
    expect(useAuthStore.getState().user).toBeDefined();
  });

  it('clears state on logout', async () => {
    useAuthStore.setState({
      user: { id: '1', email: 'test@test.com' },
      isAuthenticated: true,
    });

    await useAuthStore.getState().logout();

    expect(useAuthStore.getState().user).toBeNull();
    expect(useAuthStore.getState().isAuthenticated).toBe(false);
  });
});

Common Matchers

// Element presence
expect(element).toBeOnTheScreen();
expect(element).not.toBeOnTheScreen();

// Text content
expect(element).toHaveTextContent('Hello');

// Accessibility
expect(element).toBeEnabled();
expect(element).toBeDisabled();
expect(element).toHaveAccessibilityValue({ text: '50%' });

// Style (with jest-native)
expect(element).toHaveStyle({ backgroundColor: 'red' });

Running Tests

# Run all tests
npm test

# Run with coverage
npm test -- --coverage

# Run specific file
npm test -- Button.test.tsx

# Watch mode
npm test -- --watch

Notes

  • Use screen for queries instead of destructuring from render
  • Prefer getByRole and getByLabelText for accessibility
  • Use waitFor for async operations
  • Mock native modules in setup file
  • Test behavior, not implementation
  • Keep tests focused and isolated