Advanced Mocking Strategies and Best Practices
Beyond Basic Mocking
By now, you've learned the fundamentals of testing in Next.js applications. But real-world applications often present complex scenarios that require advanced mocking strategies. Let's explore sophisticated mocking techniques that will help you handle edge cases, complex dependencies, and maintainable test suites.
Advanced Mock Patterns
1. Conditional Mocking
Sometimes you need different mock behavior based on test conditions:
// ✅ DO: Use conditional mocking for different scenarios
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from '@/components/UserProfile';
describe('UserProfile', () => {
beforeEach(() => {
global.fetch = jest.fn();
});
it('should handle different user roles', async () => {
const mockUsers = {
admin: {
id: 1,
name: 'Admin User',
role: 'admin',
permissions: ['read', 'write', 'delete']
},
user: {
id: 2,
name: 'Regular User',
role: 'user',
permissions: ['read']
}
};
// Test admin user
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockUsers.admin)
});
const { rerender } = render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('Delete User')).toBeInTheDocument();
expect(screen.getByText('Edit Profile')).toBeInTheDocument();
});
// Test regular user
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockUsers.user)
});
rerender(<UserProfile userId={2} />);
await waitFor(() => {
expect(screen.queryByText('Delete User')).not.toBeInTheDocument();
expect(screen.getByText('View Profile')).toBeInTheDocument();
});
});
});
2. Dynamic Mock Responses
Create mocks that respond differently based on input parameters:
// ✅ DO: Create dynamic mock responses
import EmailService from '@/services/EmailService';
describe('EmailService', () => {
let emailService: EmailService;
let mockTransporter: jest.Mocked<Transporter>;
beforeEach(() => {
mockTransporter = {
sendMail: jest.fn(),
verify: jest.fn()
} as any;
emailService = new EmailService(mockTransporter);
});
it('should handle different email types', async () => {
// Create dynamic mock that responds based on email type
mockTransporter.sendMail.mockImplementation((options) => {
const { to, subject } = options;
if (subject.includes('Welcome')) {
return Promise.resolve({ messageId: 'welcome-123' });
} else if (subject.includes('Password Reset')) {
return Promise.resolve({ messageId: 'reset-456' });
} else {
return Promise.reject(new Error('Unknown email type'));
}
});
// Test welcome email
const welcomeResult = await emailService.sendWelcomeEmail({
to: 'user@example.com',
name: 'John Doe'
});
expect(welcomeResult.messageId).toBe('welcome-123');
// Test password reset email
const resetResult = await emailService.sendPasswordResetEmail({
to: 'user@example.com',
resetToken: 'token123'
});
expect(resetResult.messageId).toBe('reset-456');
});
});
3. Mock Factory Functions
Create reusable mock factories for complex objects:
// ✅ DO: Use mock factories for complex objects
const createMockUser = (overrides: Partial<User> = {}): User => ({
id: 1,
name: 'John Doe',
email: 'john@example.com',
role: 'user',
createdAt: new Date('2024-01-01'),
isActive: true,
...overrides
});
const createMockPost = (overrides: Partial<Post> = {}): Post => ({
id: 1,
title: 'Test Post',
content: 'Test content',
authorId: 1,
publishedAt: new Date('2024-01-01'),
tags: ['test'],
...overrides
});
describe('UserService', () => {
it('should handle different user scenarios', async () => {
const adminUser = createMockUser({ role: 'admin', permissions: ['all'] });
const inactiveUser = createMockUser({ isActive: false });
const newUser = createMockUser({ createdAt: new Date() });
// Test with different user types
expect(adminUser.role).toBe('admin');
expect(inactiveUser.isActive).toBe(false);
expect(newUser.createdAt).toEqual(new Date());
});
});
Complex Dependency Mocking
1. Mocking Circular Dependencies
Handle services that depend on each other:
// ✅ DO: Mock circular dependencies carefully
import UserService from '@/services/UserService';
import PostService from '@/services/PostService';
// Mock both services to break circular dependency
jest.mock('@/services/UserService');
jest.mock('@/services/PostService');
describe('Service Integration', () => {
let userService: jest.Mocked<UserService>;
let postService: jest.Mocked<PostService>;
beforeEach(() => {
jest.clearAllMocks();
// Get mocked instances
userService = new UserService() as jest.Mocked<UserService>;
postService = new PostService() as jest.Mocked<PostService>;
// Setup cross-service mocks
userService.getUserById.mockResolvedValue({
id: 1,
name: 'John Doe',
posts: []
});
postService.getPostsByUserId.mockResolvedValue([
{ id: 1, title: 'Test Post', authorId: 1 }
]);
});
it('should handle service interactions', async () => {
const user = await userService.getUserById(1);
const posts = await postService.getPostsByUserId(user.id);
expect(user.name).toBe('John Doe');
expect(posts).toHaveLength(1);
expect(posts[0].authorId).toBe(user.id);
});
});
2. Mocking Event Emitters
Test components that use event emitters:
// ✅ DO: Mock event emitters properly
import { render, screen, fireEvent } from '@testing-library/react';
import EventComponent from '@/components/EventComponent';
describe('EventComponent', () => {
let mockEventEmitter: jest.Mocked<EventEmitter>;
beforeEach(() => {
mockEventEmitter = {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
once: jest.fn()
} as any;
// Mock the event emitter module
jest.mock('@/lib/eventEmitter', () => ({
default: mockEventEmitter
}));
});
it('should listen to events', () => {
render(<EventComponent />);
expect(mockEventEmitter.on).toHaveBeenCalledWith('userUpdate', expect.any(Function));
});
it('should emit events on user interaction', () => {
render(<EventComponent />);
fireEvent.click(screen.getByRole('button', { name: /update/i }));
expect(mockEventEmitter.emit).toHaveBeenCalledWith('userAction', {
type: 'update',
timestamp: expect.any(Date)
});
});
});
Advanced API Mocking
1. Mocking GraphQL Queries
Test components that use GraphQL:
// ✅ DO: Mock GraphQL queries and mutations
import { render, screen, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { gql } from '@apollo/client';
import UserList from '@/components/UserList';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
const mocks = [
{
request: {
query: GET_USERS
},
result: {
data: {
users: [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
]
}
}
}
];
describe('UserList', () => {
it('should render users from GraphQL', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<UserList />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});
});
});
2. Mocking WebSocket Connections
Test real-time components:
// ✅ DO: Mock WebSocket connections
import { render, screen, waitFor } from '@testing-library/react';
import ChatComponent from '@/components/ChatComponent';
describe('ChatComponent', () => {
let mockWebSocket: jest.Mocked<WebSocket>;
beforeEach(() => {
// Mock WebSocket
mockWebSocket = {
send: jest.fn(),
close: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
readyState: WebSocket.OPEN
} as any;
global.WebSocket = jest.fn(() => mockWebSocket) as any;
});
it('should connect to WebSocket', () => {
render(<ChatComponent />);
expect(global.WebSocket).toHaveBeenCalledWith('ws://localhost:3001/chat');
expect(mockWebSocket.addEventListener).toHaveBeenCalledWith('message', expect.any(Function));
});
it('should handle incoming messages', async () => {
render(<ChatComponent />);
// Simulate incoming message
const messageEvent = new MessageEvent('message', {
data: JSON.stringify({ type: 'chat', message: 'Hello!' })
});
// Get the message handler
const messageHandler = mockWebSocket.addEventListener.mock.calls.find(
call => call[0] === 'message'
)?.[1];
if (messageHandler) {
messageHandler(messageEvent);
}
await waitFor(() => {
expect(screen.getByText('Hello!')).toBeInTheDocument();
});
});
});
Time and Date Mocking
1. Mocking Date and Time
Test time-dependent functionality:
// ✅ DO: Mock dates for predictable tests
import { render, screen } from '@testing-library/react';
import TimeComponent from '@/components/TimeComponent';
describe('TimeComponent', () => {
beforeEach(() => {
// Mock Date to return a fixed timestamp
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-01T12:00:00Z'));
});
afterEach(() => {
jest.useRealTimers();
});
it('should display current time', () => {
render(<TimeComponent />);
expect(screen.getByText(/12:00:00/)).toBeInTheDocument();
});
it('should update time every second', () => {
render(<TimeComponent />);
// Advance time by 1 second
jest.advanceTimersByTime(1000);
expect(screen.getByText(/12:00:01/)).toBeInTheDocument();
});
});
2. Mocking setTimeout and setInterval
Test components with timers:
// ✅ DO: Mock timers for predictable testing
import { render, screen, waitFor } from '@testing-library/react';
import TimerComponent from '@/components/TimerComponent';
describe('TimerComponent', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should show countdown', () => {
render(<TimerComponent duration={5} />);
expect(screen.getByText('5')).toBeInTheDocument();
// Advance timer
jest.advanceTimersByTime(1000);
expect(screen.getByText('4')).toBeInTheDocument();
});
it('should call onComplete when timer finishes', () => {
const onComplete = jest.fn();
render(<TimerComponent duration={3} onComplete={onComplete} />);
// Advance timer to completion
jest.advanceTimersByTime(3000);
expect(onComplete).toHaveBeenCalled();
});
});
File System Mocking
1. Mocking File Operations
Test file upload and processing:
// ✅ DO: Mock file system operations
import { render, screen, fireEvent } from '@testing-library/react';
import FileUpload from '@/components/FileUpload';
describe('FileUpload', () => {
beforeEach(() => {
// Mock File API
global.File = jest.fn().mockImplementation((content, filename) => ({
name: filename,
size: content.length,
type: 'text/plain',
text: () => Promise.resolve(content)
})) as any;
// Mock FileReader
global.FileReader = jest.fn().mockImplementation(() => ({
readAsText: jest.fn(),
result: 'file content',
onload: null
})) as any;
});
it('should handle file upload', async () => {
render(<FileUpload />);
const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
const input = screen.getByLabelText(/upload file/i);
fireEvent.change(input, { target: { files: [file] } });
await waitFor(() => {
expect(screen.getByText('test.txt uploaded')).toBeInTheDocument();
});
});
});
Database Mocking Strategies
1. Mocking Database Transactions
Test database operations:
// ✅ DO: Mock database transactions
import UserService from '@/services/UserService';
describe('UserService', () => {
let mockDb: jest.Mocked<Database>;
let userService: UserService;
beforeEach(() => {
mockDb = {
beginTransaction: jest.fn(),
commit: jest.fn(),
rollback: jest.fn(),
query: jest.fn()
} as any;
userService = new UserService(mockDb);
});
it('should handle transaction success', async () => {
mockDb.beginTransaction.mockResolvedValue();
mockDb.query.mockResolvedValue([{ id: 1 }]);
mockDb.commit.mockResolvedValue();
await userService.createUserWithProfile({
name: 'John Doe',
email: 'john@example.com',
profile: { bio: 'Test bio' }
});
expect(mockDb.beginTransaction).toHaveBeenCalled();
expect(mockDb.commit).toHaveBeenCalled();
expect(mockDb.rollback).not.toHaveBeenCalled();
});
it('should rollback on error', async () => {
mockDb.beginTransaction.mockResolvedValue();
mockDb.query.mockRejectedValue(new Error('Database error'));
mockDb.rollback.mockResolvedValue();
await expect(
userService.createUserWithProfile({
name: 'John Doe',
email: 'john@example.com',
profile: { bio: 'Test bio' }
})
).rejects.toThrow('Database error');
expect(mockDb.rollback).toHaveBeenCalled();
expect(mockDb.commit).not.toHaveBeenCalled();
});
});
Performance Testing with Mocks
1. Mocking Performance APIs
Test performance-sensitive code:
// ✅ DO: Mock performance APIs
import { render, screen } from '@testing-library/react';
import PerformanceComponent from '@/components/PerformanceComponent';
describe('PerformanceComponent', () => {
beforeEach(() => {
// Mock performance.now()
jest.spyOn(performance, 'now').mockReturnValue(1000);
// Mock requestAnimationFrame
global.requestAnimationFrame = jest.fn(cb => {
setTimeout(cb, 0);
return 1;
});
});
it('should measure render time', () => {
render(<PerformanceComponent />);
expect(screen.getByText(/render time: 0ms/)).toBeInTheDocument();
});
});
Best Practices for Complex Mocking
1. Mock Organization
// ✅ DO: Organize mocks in a structured way
// __mocks__/services/__mocks__.ts
export const createMockUserService = () => ({
getUserById: jest.fn(),
createUser: jest.fn(),
updateUser: jest.fn(),
deleteUser: jest.fn()
});
export const createMockPostService = () => ({
getPosts: jest.fn(),
createPost: jest.fn(),
updatePost: jest.fn(),
deletePost: jest.fn()
});
// In your test file
import { createMockUserService, createMockPostService } from '@/__mocks__/services';
describe('ComplexComponent', () => {
let mockUserService: ReturnType<typeof createMockUserService>;
let mockPostService: ReturnType<typeof createMockPostService>;
beforeEach(() => {
mockUserService = createMockUserService();
mockPostService = createMockPostService();
});
});
2. Mock Cleanup
// ✅ DO: Clean up mocks properly
describe('ComplexTest', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
});
afterEach(() => {
jest.restoreAllMocks();
});
afterAll(() => {
jest.resetAllMocks();
});
});
3. Mock Validation
// ✅ DO: Validate mock calls
it('should call service with correct parameters', async () => {
const mockService = createMockService();
await component.doSomething();
expect(mockService.method).toHaveBeenCalledWith(
expect.objectContaining({
id: expect.any(Number),
name: expect.any(String)
})
);
// Verify call count
expect(mockService.method).toHaveBeenCalledTimes(1);
});
Common Advanced Mocking Pitfalls
❌ DON'T: Over-Mock Complex Systems
// ❌ WRONG: Mocking everything in a complex system
jest.mock('@/services/UserService');
jest.mock('@/services/PostService');
jest.mock('@/services/CommentService');
jest.mock('@/services/NotificationService');
jest.mock('@/services/AnalyticsService');
// This creates a system that does nothing real
❌ DON'T: Ignore Mock Side Effects
// ❌ WRONG: Not considering mock side effects
mockService.method.mockImplementation(() => {
// This might have side effects that affect other tests
global.someState = 'modified';
return 'result';
});
❌ DON'T: Mock What You're Testing
// ❌ WRONG: Mocking the component you're testing
jest.mock('@/components/UserProfile', () => ({
default: jest.fn().mockReturnValue(<div>Mocked UserProfile</div>)
}));
// This tests the mock, not the real component
The Journey Continues
You've now mastered advanced mocking strategies for Next.js applications. These techniques will help you handle complex testing scenarios and maintain reliable test suites.
Advanced Mocking Checklist
For Complex Dependencies:
- [ ] Use mock factories for reusable mocks
- [ ] Handle circular dependencies carefully
- [ ] Mock at the right abstraction level
- [ ] Validate mock calls and parameters
For Time-Dependent Code:
- [ ] Mock dates and timers consistently
- [ ] Use fake timers for predictable tests
- [ ] Clean up timer mocks after tests
- [ ] Test time-based behavior thoroughly
For External Systems:
- [ ] Mock APIs and databases appropriately
- [ ] Test error conditions and edge cases
- [ ] Verify transaction handling
- [ ] Mock performance APIs when needed
For Maintainability:
- [ ] Organize mocks in a structured way
- [ ] Clean up mocks between tests
- [ ] Document complex mock setups
- [ ] Avoid over-mocking
Remember: Advanced mocking should make your tests more reliable, not more complex.
This concludes our unit testing journey in Next.js. You now have the knowledge and patterns to write comprehensive, reliable tests for any Next.js application.