Skip to main content

The Mocking Playbook

This app's components lean on heavyweight dependencies — an ArcGIS runtime, a live SignalR socket, thirty Zustand stores, an authenticated session. None of those belong in a unit test, so nearly every test starts by swapping some of them for fakes. This page is the playbook: one recipe per dependency type, in the order you're likely to need them. Treat it as a reference — find your dependency, copy the recipe, adjust.

The SWC trap every jest.mock factory hits eventually

jest.mock factories are hoisted above your imports, and this repo compiles tests with SWC — so a factory that closes over an outer variable throws a temporal-dead-zone error at runtime. Define the mock's state inside the factory and expose it for assertions:

jest.mock('next/navigation', () => {
const state = { pushedUrl: null as string | null };
return {
__esModule: true,
__state: state,
useRouter: () => ({
push: (url: string) => {
state.pushedUrl = url;
},
}),
};
});

The skipped GraphicNotification suite is the in-repo cautionary tale — see its doc for the full story.

Types of Mocks

1. CSS Module Mocks

Mock CSS modules to avoid import errors and enable class name assertions:

jest.mock('./MyComponent.module.css', () => ({
container: 'container-class',
active: 'active-class',
hidden: 'hidden-class',
}));

// In tests
expect(element).toHaveClass('active-class');

2. Component Mocks

Mock child components to isolate the component under test:

// Mock a complex child component
jest.mock('@/components/molecules/MapPolygonCreation', () => ({
__esModule: true,
default: ({ onChange }: { onChange?: (value: string) => void }) => (
<div data-testid="mock-map-polygon">Mock MapPolygonCreation</div>
),
}));

// Mock with forwardRef
jest.mock('@/components/molecules/MapPolygonFL', () => ({
__esModule: true,
default: React.forwardRef(() => (
<div data-testid="map-polygon">Map Polygon</div>
)),
}));

3. Material-UI Icon Mocks

Mock MUI icons to simplify rendering:

jest.mock('@mui/icons-material/MoreHoriz', () => ({
__esModule: true,
default: () => <div data-testid="more-horiz-icon">MoreHorizIcon</div>,
}));

jest.mock('@mui/material/SvgIcon', () => {
return function MockSvgIcon({ children, viewBox }: any) {
return (
<svg data-testid="svg-icon" data-viewbox={viewBox}>
{children}
</svg>
);
};
});

4. Next.js Mocks

Router Mock

// In individual test file
jest.mock('next/router', () => ({
useRouter: jest.fn().mockReturnValue({
pathname: '/',
push: jest.fn(),
query: {},
asPath: '/',
replace: jest.fn(),
back: jest.fn(),
}),
}));

// Override in specific test
import { useRouter } from 'next/router';

beforeEach(() => {
(useRouter as jest.Mock).mockReturnValue({
pathname: '/camera/123',
query: { id: '123' },
push: jest.fn(),
});
});
jest.mock('next/navigation', () => ({
useRouter: jest.fn(() => ({
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
})),
usePathname: jest.fn(() => '/'),
useSearchParams: jest.fn(() => new URLSearchParams()),
}));

5. NextAuth Session Mocks

import { useSession } from 'next-auth/react';

jest.mock('next-auth/react', () => ({
useSession: jest.fn(),
SessionProvider: ({ children }: any) => children,
signIn: jest.fn(),
signOut: jest.fn(),
}));

describe('Component with auth', () => {
test('renders for authenticated user', () => {
(useSession as jest.Mock).mockReturnValue({
data: {
user: {
id: '123',
name: 'John Doe',
roles: ['admin']
},
expires: new Date(Date.now() + 86400000).toISOString(),
},
status: 'authenticated',
});

render(<ProtectedComponent />);
expect(screen.getByText('Welcome, John Doe')).toBeInTheDocument();
});

test('shows login prompt for unauthenticated user', () => {
(useSession as jest.Mock).mockReturnValue({
data: null,
status: 'unauthenticated',
});

render(<ProtectedComponent />);
expect(screen.getByText('Please sign in')).toBeInTheDocument();
});
});

6. Zustand Store Mocks

Global Store Mock (in __mocks__/)

// __mocks__/stores/cameraOptionsStore.ts
export const useCameraOptionsStore = () => ({
cameraListLayoutOption: 'grid',
setCameraListLayoutOption: jest.fn(),
cameraOptions: [
{ id: 1, name: 'Front Door', enabled: true },
{ id: 2, name: 'Backyard', enabled: false },
],
addCameraOption: jest.fn(),
});

export const useShallow = jest.fn();

Per-Test Store Mock

import { useMapCameraInteractiveStore } from '@/stores/mapCameraInteractiveStore';

jest.mock('@/stores/mapCameraInteractiveStore', () => ({
useMapCameraInteractiveStore: jest.fn(),
}));

describe('Component with store', () => {
beforeEach(() => {
(useMapCameraInteractiveStore as unknown as jest.Mock).mockReturnValue({
selectedCamera: null,
setSelectedCamera: jest.fn(),
isLoading: false,
});
});

test('shows camera details when selected', () => {
(useMapCameraInteractiveStore as unknown as jest.Mock).mockReturnValue({
selectedCamera: { id: '123', name: 'Test Camera' },
setSelectedCamera: jest.fn(),
isLoading: false,
});

render(<CameraPanel />);
expect(screen.getByText('Test Camera')).toBeInTheDocument();
});
});

7. Context Mocks

describe('Component with context', () => {
const mockMapView = {
container: document.createElement('div'),
zoom: 10,
};

const mockMapClass = {
getView: jest.fn(() => mockMapView),
setCenter: jest.fn(),
};

beforeEach(() => {
(useMapContext as jest.Mock).mockReturnValue({
mapClass: mockMapClass,
});
});

test('renders map component', () => {
render(<MapWidget />);
expect(mockMapClass.getView).toHaveBeenCalled();
});
});

8. ArcGIS/ESRI Mocks

// Mock ESRI widgets
const mockCoordinateConversion = {
destroy: jest.fn(),
visible: true,
};

jest.mock('@arcgis/core/widgets/CoordinateConversion', () => ({
default: jest.fn().mockImplementation(() => mockCoordinateConversion),
}));

// Mock constants that import ESRI
jest.mock('@/data/constants', () => ({
BASIC_TOPO_MAP_PORTAL_ITEM_ID: 'mock-id',
DEFAULT_CENTER: [0, 0],
DEFAULT_ZOOM_LOCATION_VIEW: 10,
}));

9. API/Fetch Mocks

// Mock global fetch
global.fetch = jest.fn();

beforeEach(() => {
(global.fetch as jest.Mock).mockReset();
});

test('fetches and displays data', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({ cameras: [{ id: '1', name: 'Camera 1' }] }),
});

render(<CameraList />);

await waitFor(() => {
expect(screen.getByText('Camera 1')).toBeInTheDocument();
});

expect(global.fetch).toHaveBeenCalledWith('/api/cameras');
});

test('handles fetch error', async () => {
(global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'));

render(<CameraList />);

await waitFor(() => {
expect(screen.getByText(/error loading cameras/i)).toBeInTheDocument();
});
});

10. React Query Hook Mocks

import { useGetCameras } from '@/queries/useGetCameras';

jest.mock('@/queries/useGetCameras');

describe('Component with React Query', () => {
test('shows loading state', () => {
(useGetCameras as jest.Mock).mockReturnValue({
data: undefined,
isLoading: true,
error: null,
});

render(<CameraList />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});

test('shows data when loaded', () => {
(useGetCameras as jest.Mock).mockReturnValue({
data: [{ id: '1', name: 'Camera 1' }],
isLoading: false,
error: null,
});

render(<CameraList />);
expect(screen.getByText('Camera 1')).toBeInTheDocument();
});

test('shows error state', () => {
(useGetCameras as jest.Mock).mockReturnValue({
data: undefined,
isLoading: false,
error: new Error('Failed to fetch'),
});

render(<CameraList />);
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});

Mock File Organization

Directory Structure

__mocks__/
├── @arcgis/ # ArcGIS library mocks
│ └── core/
│ └── widgets/
├── stores/ # Zustand store mocks
│ ├── cameraOptionsStore.ts
│ └── configStore.ts
└── chalk.js # Third-party library mock

When to Use Global vs Local Mocks

ScenarioLocationExample
Used in many test files__mocks__/ directoryStores, router, session
Used in one test fileInside the test fileSpecific component mock
Per-test customizationbeforeEach blockDifferent store states

Mock Function Utilities

// Create a mock function
const mockFn = jest.fn();

// Mock with return value
const mockFn = jest.fn().mockReturnValue('value');

// Mock with resolved promise
const mockFn = jest.fn().mockResolvedValue({ data: 'value' });

// Mock with rejected promise
const mockFn = jest.fn().mockRejectedValue(new Error('error'));

// Mock implementation
const mockFn = jest.fn().mockImplementation(arg => arg * 2);

// Assert calls
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('lastArg');

// Clear mock calls (keeps implementation)
mockFn.mockClear();

// Reset mock (clears calls and implementation)
mockFn.mockReset();

// Restore original implementation
mockFn.mockRestore();

Best Practices

  1. Clear mocks in beforeEach - Prevents test pollution
  2. Mock at the right level - Mock dependencies, not the unit under test
  3. Use global mocks sparingly - Keep test files self-contained when possible
  4. Document complex mocks - Add comments explaining mock behavior
  5. Match mock structure to real implementation - Return the same shape of data