mocking-strategies

star 3

WHAT: Mock external dependencies in React Native tests with jest.mock and factory patterns. WHEN: testing TanStack Query, Apollo GraphQL, React Navigation, Zustand stores, native modules. KEYWORDS: mock, jest.mock, TanStack Query, Apollo, navigation, Zustand, native modules, clearAllMocks, builder, factory.

guicheffer By guicheffer schedule Updated 12/30/2025

name: mocking-strategies description: "WHAT: Mock external dependencies in React Native tests with jest.mock and factory patterns. WHEN: testing TanStack Query, Apollo GraphQL, React Navigation, Zustand stores, native modules. KEYWORDS: mock, jest.mock, TanStack Query, Apollo, navigation, Zustand, native modules, clearAllMocks, builder, factory."

Mocking Strategies for React Native Testing

Documentation

This skill has comprehensive documentation:

Core Principles

Mock external dependencies only, test internal logic. Replace network calls, navigation, native modules, and third-party libraries with controlled test doubles. Never mock internal utilities or helpers.

Why: Proper mocking enables fast, isolated, reliable tests that focus on component logic without external dependencies. Test behavior, not implementation.

When to Use This Skill

Use these mocking strategies when testing:

  • Components using TanStack Query or Apollo Client
  • Navigation behavior with React Navigation
  • Components reading from Zustand stores
  • Native module integrations (device info, analytics)
  • Platform-specific code (iOS vs Android)
  • Responsive layouts with window dimensions
  • Components with complex external dependencies

Mocking TanStack Query

Mock useQuery Return Values

Test different data states without real API calls:

import { useQuery } from '@tanstack/react-query';

jest.mock('@tanstack/react-query', () => ({
  useQuery: jest.fn(),
  useQueryClient: jest.fn(),
}));

describe('Component using useQuery', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should handle loading state', () => {
    (useQuery as jest.Mock).mockReturnValue({
      data: undefined,
      isLoading: true,
      error: null,
    });

    const { result } = renderHook(() => useMyHook());

    expect(result.current.isLoading).toBe(true);
  });

  it('should handle success state with data', () => {
    const mockData = { id: '123', name: 'Test' };

    (useQuery as jest.Mock).mockReturnValue({
      data: mockData,
      isLoading: false,
      error: null,
    });

    const { result } = renderHook(() => useMyHook());

    expect(result.current.data).toEqual(mockData);
  });

  it('should handle error state', () => {
    const mockError = new Error('Network error');

    (useQuery as jest.Mock).mockReturnValue({
      data: undefined,
      isLoading: false,
      error: mockError,
    });

    const { result } = renderHook(() => useMyHook());

    expect(result.current.error).toBe(mockError);
  });
});

Why: Configure different states per test to verify loading, success, and error handling.

Mock useMutation

Test mutations without triggering real API calls:

import { useMutation } from '@tanstack/react-query';

jest.mock('@tanstack/react-query', () => ({
  useMutation: jest.fn(),
}));

describe('Component using useMutation', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should call mutation with correct data', async () => {
    const mockMutate = jest.fn();

    (useMutation as jest.Mock).mockReturnValue({
      mutate: mockMutate,
      isLoading: false,
      isSuccess: false,
      isError: false,
    });

    const { result } = renderHook(() => useSaveRecipe());

    act(() => {
      result.current.mutate({ url: 'https://example.com/recipe' });
    });

    expect(mockMutate).toHaveBeenCalledWith({
      url: 'https://example.com/recipe',
    });
  });

  it('should handle mutation success', () => {
    (useMutation as jest.Mock).mockReturnValue({
      mutate: jest.fn(),
      isLoading: false,
      isSuccess: true,
      data: { id: '123', title: 'Recipe' },
    });

    const { result } = renderHook(() => useSaveRecipe());

    expect(result.current.isSuccess).toBe(true);
    expect(result.current.data).toEqual({ id: '123', title: 'Recipe' });
  });
});

Why: Test optimistic updates, error handling, and success states in isolation.

Mocking Apollo Client (GraphQL)

Mock useQuery Hook

Test GraphQL operations without a server:

import { useQuery } from '@apollo/client';

jest.mock('@apollo/client', () => ({
  useQuery: jest.fn(),
  gql: jest.fn((query) => query),
}));

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

  it('should handle loading state', () => {
    (useQuery as jest.Mock).mockReturnValue({
      data: undefined,
      loading: true,
      error: undefined,
    });

    const { getByTestId } = render(<MyComponent />);

    expect(getByTestId('loading-spinner')).toBeDefined();
  });

  it('should render data from query', () => {
    const mockData = {
      recipes: [
        { id: '1', title: 'Pasta' },
        { id: '2', title: 'Salad' },
      ],
    };

    (useQuery as jest.Mock).mockReturnValue({
      data: mockData,
      loading: false,
      error: undefined,
    });

    const { getByText } = render(<RecipeList />);

    expect(getByText('Pasta')).toBeDefined();
    expect(getByText('Salad')).toBeDefined();
  });

  it('should handle query errors', () => {
    const mockError = new Error('GraphQL Error');

    (useQuery as jest.Mock).mockReturnValue({
      data: undefined,
      loading: false,
      error: mockError,
    });

    const { getByText } = render(<MyComponent />);

    expect(getByText(/error/i)).toBeDefined();
  });
});

Why: Mock GraphQL responses to test data fetching without network dependencies.

Mocking React Navigation

Mock useNavigation Hook

Test navigation behavior without navigation stack:

import { useNavigation } from '@react-navigation/native';

jest.mock('@react-navigation/native', () => ({
  useNavigation: jest.fn(),
  useRoute: jest.fn(),
  useFocusEffect: jest.fn(),
}));

describe('Component using navigation', () => {
  const mockNavigate = jest.fn();
  const mockGoBack = jest.fn();

  beforeEach(() => {
    (useNavigation as jest.Mock).mockReturnValue({
      navigate: mockNavigate,
      goBack: mockGoBack,
      setOptions: jest.fn(),
    });

    jest.clearAllMocks();
  });

  it('should navigate to detail screen with params', () => {
    const { getByTestId } = render(<RecipeCard recipe={mockRecipe} />);

    fireEvent.press(getByTestId('recipe-card'));

    expect(mockNavigate).toHaveBeenCalledWith('RecipeDetail', {
      recipeId: mockRecipe.id,
    });
  });

  it('should go back when close button pressed', () => {
    const { getByTestId } = render(<DetailScreen />);

    fireEvent.press(getByTestId('close-button'));

    expect(mockGoBack).toHaveBeenCalled();
  });
});

Why: Test navigation actions without actual stack manipulation.

Mock useRoute for Route Params

Test screens with different parameter combinations:

import { useRoute } from '@react-navigation/native';

jest.mock('@react-navigation/native', () => ({
  useRoute: jest.fn(),
  useNavigation: jest.fn(),
}));

describe('Screen with route params', () => {
  it('should use route params', () => {
    const mockParams = {
      recipeId: '123',
      source: 'favorites',
    };

    (useRoute as jest.Mock).mockReturnValue({
      params: mockParams,
      name: 'RecipeDetail',
    });

    const { getByText } = render(<RecipeDetailScreen />);

    expect(getByText(/Recipe 123/)).toBeDefined();
  });
});

Why: Test screens with various param combinations without navigation.

Mocking Zustand Stores

Mock Store Return Values

Test components in isolation from global state:

import { useProductStore } from '@modules/store/zustand-store';

jest.mock('zustand/react/shallow', () => ({
  shallow: jest.fn((fn) => fn),
}));

jest.mock('@modules/store/zustand-store', () => ({
  useProductStore: jest.fn(),
}));

describe('Component with Zustand store', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should render with store data', () => {
    (useProductStore as jest.Mock).mockReturnValue({
      selectedProducts: ['product-1', 'product-2'],
      addProduct: jest.fn(),
      removeProduct: jest.fn(),
    });

    const { getByText } = render(<ProductList />);

    expect(getByText('2 products selected')).toBeDefined();
  });

  it('should call store action on button press', () => {
    const mockAddProduct = jest.fn();

    (useProductStore as jest.Mock).mockReturnValue({
      selectedProducts: [],
      addProduct: mockAddProduct,
      removeProduct: jest.fn(),
    });

    const { getByTestId } = render(<ProductCard productId="123" />);

    fireEvent.press(getByTestId('add-button'));

    expect(mockAddProduct).toHaveBeenCalledWith('123');
  });
});

Why: Isolate components from global state, test with various store states.

Mocking Native Modules

Mock react-native-device-info

Provide consistent device data:

jest.mock('react-native-device-info', () => ({
  getBundleId: () => 'com.yourcompany.app',
  getSystemName: () => 'iOS',
  getSystemVersion: () => '17.0',
  getVersion: () => '5.2.1',
  getBrand: () => 'Apple',
  getApplicationName: () => 'YourCompany',
  getBuildNumber: () => '123',
  getDeviceId: () => 'iPhone14,2',
}));

describe('Device Info Component', () => {
  it('should display device information', () => {
    const { getByText } = render(<DeviceInfo />);

    expect(getByText(/iOS 17.0/)).toBeDefined();
    expect(getByText(/YourCompany 5.2.1/)).toBeDefined();
  });
});

Why: Consistent device data across test environments.

Mock Custom Native Modules

Prevent errors when native code unavailable:

jest.mock('@data-access/native', () => ({
  AppConfigDataAccess: {
    queries: {
      useBrandState: jest.fn().mockReturnValue({ data: 'yourcompany' }),
      useSystemCountry: jest.fn().mockReturnValue({ data: 'US' }),
    },
  },
}));

jest.mock('@libs/native-modules/analytics-tracker', () => ({
  SharedModulesAnalyticsTracker: {
    trackAnalyticsEvent: jest.fn(),
    trackOpenScreenEvent: jest.fn(),
  },
  AnalyticsEvent: jest.fn().mockImplementation((params) => params),
}));

describe('Component with native modules', () => {
  const mockTrackEvent = SharedModulesAnalyticsTracker.trackAnalyticsEvent as jest.Mock;

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should track event using native module', () => {
    const { getByTestId } = render(<TrackedButton />);

    fireEvent.press(getByTestId('button'));

    expect(mockTrackEvent).toHaveBeenCalledWith({
      eventName: 'button_clicked',
      parameters: expect.any(Object),
    });
  });
});

Why: Native modules throw errors in test environment without mocks.

Mocking React Native Core

Mock useWindowDimensions

Test responsive layouts:

import { useWindowDimensions } from 'react-native';

jest.mock('react-native', () => ({
  ...jest.requireActual('react-native'),
  useWindowDimensions: jest.fn(),
}));

describe('Responsive component', () => {
  it('should render mobile layout on small screen', () => {
    (useWindowDimensions as jest.Mock).mockReturnValue({
      width: 375,
      height: 667,
      scale: 2,
      fontScale: 1,
    });

    const { getByTestId } = render(<ResponsiveLayout />);

    expect(getByTestId('mobile-layout')).toBeDefined();
  });

  it('should render tablet layout on large screen', () => {
    (useWindowDimensions as jest.Mock).mockReturnValue({
      width: 1024,
      height: 768,
      scale: 2,
      fontScale: 1,
    });

    const { getByTestId } = render(<ResponsiveLayout />);

    expect(getByTestId('tablet-layout')).toBeDefined();
  });
});

Why: Test responsive behavior without changing device settings.

Mock Platform

Test platform-specific code:

import { Platform } from 'react-native';

describe('Platform-specific component', () => {
  const originalOS = Platform.OS;

  afterEach(() => {
    Platform.OS = originalOS; // Restore original
  });

  it('should render iOS-specific component', () => {
    Platform.OS = 'ios';

    const { getByTestId } = render(<PlatformComponent />);

    expect(getByTestId('ios-feature')).toBeDefined();
  });

  it('should render Android-specific component', () => {
    Platform.OS = 'android';

    const { getByTestId } = render(<PlatformComponent />);

    expect(getByTestId('android-feature')).toBeDefined();
  });
});

Why: Test both platforms without multiple test environments. Always restore in afterEach.

Test Helper Patterns

Builder Pattern for Complex Setup

Reduce boilerplate in complex tests:

class AnalyticsTestBuilder {
  private contextProps: Record<string, unknown> = {};
  private brandData = 'yourcompany';

  withContextData(data: Record<string, unknown>) {
    this.contextProps = data;
    return this;
  }

  withBrand(brand: string) {
    this.brandData = brand;
    return this;
  }

  build() {
    mockUseBrandState.mockReturnValue({ data: this.brandData });

    if (Object.keys(this.contextProps).length > 0) {
      const Wrapper = ({ children }: React.PropsWithChildren) => (
        <AnalyticsProvider {...this.contextProps}>{children}</AnalyticsProvider>
      );

      return renderHook(() => useAnalyticsTracker(), { wrapper: Wrapper });
    }

    return renderHook(() => useAnalyticsTracker());
  }
}

// Usage
const { result } = new AnalyticsTestBuilder()
  .withContextData({ screenName: 'Store' })
  .withBrand('greenchef')
  .build();

Why: Builder pattern makes complex setup readable and maintainable.

Mock Factory Functions

Create consistent mocks with defaults:

const createMockNavigation = (overrides = {}) => ({
  navigate: jest.fn(),
  goBack: jest.fn(),
  setOptions: jest.fn(),
  ...overrides,
});

const createMockRoute = (params = {}) => ({
  params,
  name: 'TestScreen',
  key: 'test-key',
});

// Usage
const mockNavigation = createMockNavigation({
  navigate: jest.fn((screen) => console.log('Navigating to', screen)),
});

const mockRoute = createMockRoute({
  recipeId: '123',
  source: 'favorites',
});

Why: Factories provide consistent mocks with sensible defaults and easy overrides.

Common Mistakes to Avoid

Don't forget to clear mocks between tests:

describe('Tests with mocks', () => {
  const mockFn = jest.fn();

  it('first test', () => {
    mockFn();
    expect(mockFn).toHaveBeenCalledTimes(1);
  });

  it('second test', () => {
    // ❌ mockFn.mock.calls still has calls from first test
    expect(mockFn).toHaveBeenCalledTimes(0); // Fails!
  });
});

Do clear mocks in beforeEach:

describe('Tests with mocks', () => {
  const mockFn = jest.fn();

  beforeEach(() => {
    jest.clearAllMocks(); // Clear all mocks
  });

  it('first test', () => {
    mockFn();
    expect(mockFn).toHaveBeenCalledTimes(1);
  });

  it('second test', () => {
    expect(mockFn).toHaveBeenCalledTimes(0); // Passes!
  });
});

Don't mock too much:

// ❌ Overmocking makes tests brittle
jest.mock('../../utils');
jest.mock('../../hooks');
jest.mock('../../components');
jest.mock('../../services');

Do mock only external dependencies:

// ✅ Mock external APIs and modules, test internal logic
jest.mock('@tanstack/react-query');
jest.mock('@libs/networking-client');

// Don't mock: local utils, helpers, or components

Don't use hardcoded mock return values everywhere:

// ❌ Same mock data in every test
jest.mock('@data-access/query', () => ({
  useRecipes: () => ({ data: mockRecipes, isLoading: false }),
}));

Do configure mocks per test:

// ✅ Configure mock differently per test
jest.mock('@data-access/query');

describe('Recipe list', () => {
  it('shows loading state', () => {
    (useRecipes as jest.Mock).mockReturnValue({
      data: undefined,
      isLoading: true,
    });
    // Test loading state
  });

  it('shows recipes', () => {
    (useRecipes as jest.Mock).mockReturnValue({
      data: mockRecipes,
      isLoading: false,
    });
    // Test data display
  });
});

Quick Reference

TanStack Query: Mock useQuery/useMutation return values per test

Apollo Client: Mock useQuery with loading/data/error states

Navigation: Mock useNavigation and useRoute hooks

Zustand: Mock store hooks with different state combinations

Native Modules: Mock in jest.setup.ts or per-test file

Platform: Set Platform.OS, restore in afterEach

Best Practices:

  • ✅ Clear mocks in beforeEach(() => jest.clearAllMocks())
  • ✅ Mock external dependencies only
  • ✅ Configure mocks per test for different states
  • ✅ Use builder pattern for complex setups
  • ✅ Create factory functions for consistent mocks
  • ✅ Test behavior, not implementation

Key Libraries:

  • Jest 29.7.0
  • @testing-library/react-native 12.9.0
  • TanStack Query 5.59.16
  • Apollo Client 3.13.6

For production examples, see references/examples.md.

Install via CLI
npx skills add https://github.com/guicheffer/devorch-cli --skill mocking-strategies
Repository Details
star Stars 3
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator