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.
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(),
});
});
Navigation Mock (App Router)
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
| Scenario | Location | Example |
|---|---|---|
| Used in many test files | __mocks__/ directory | Stores, router, session |
| Used in one test file | Inside the test file | Specific component mock |
| Per-test customization | beforeEach block | Different 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
- Clear mocks in
beforeEach- Prevents test pollution - Mock at the right level - Mock dependencies, not the unit under test
- Use global mocks sparingly - Keep test files self-contained when possible
- Document complex mocks - Add comments explaining mock behavior
- Match mock structure to real implementation - Return the same shape of data