Skip to content

Mocking in Tests

Mocking replaces real dependencies (databases, APIs, email services, clocks) with controlled substitutes so tests run fast, in isolation, and without side effects.

TypeDescription
StubReturns a hardcoded value — no verification
MockRecords calls so you can assert they happened
FakeWorking implementation (e.g., in-memory database)
SpyWraps real implementation, records calls
DummyPlaceholder that’s never actually called

Most frameworks blur these lines — “mock” often means “stub + mock” combined.

Mock when:

  • The dependency is slow (database, external API)
  • The dependency has side effects (email, SMS, payments)
  • The dependency is non-deterministic (current time, random numbers)
  • You want to test a specific error path that’s hard to trigger

Don’t mock:

  • Simple value objects or data structures
  • Pure functions with no dependencies
  • Your own code that you’re testing
// Auto-mock an entire module
jest.mock('./emailService');
import { sendEmail } from './emailService';
it('sends welcome email on registration', async () => {
await userService.register({ email: 'alice@example.com' });
expect(sendEmail).toHaveBeenCalledWith({
to: 'alice@example.com',
subject: 'Welcome!',
});
});
// Control return value
(sendEmail as jest.Mock).mockResolvedValueOnce({ success: true });
// Mock only part of a module
jest.mock('./utils', () => ({
...jest.requireActual('./utils'), // keep other exports real
generateId: jest.fn().mockReturnValue('fixed-id'),
}));
// Jest — freeze time
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-01-15'));
const result = getExpiryDate(7); // adds 7 days
expect(result).toEqual(new Date('2025-01-22'));
jest.useRealTimers(); // restore after test

Mock Service Worker intercepts HTTP requests at the network level:

Terminal window
npm install -D msw
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({ id: params.id, name: 'Alice' });
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it('displays user name', async () => {
render(<UserProfile userId="1" />);
expect(await screen.findByText('Alice')).toBeInTheDocument();
});
# Mock a method on an object
def test_place_order(mocker):
mock_save = mocker.patch.object(OrderRepository, 'save', return_value=42)
order_id = order_service.place_order(user_id=1, product_id=10)
assert order_id == 42
mock_save.assert_called_once()
# Mock a function in a module
def test_send_notification(mocker):
mock_send = mocker.patch('myapp.notifications.send_sms')
order_service.complete_order(order_id=42)
mock_send.assert_called_with('+44...', 'Your order is complete!')
# Mock datetime.now()
from datetime import datetime
def test_token_expiry(mocker):
fixed_time = datetime(2025, 1, 15, 12, 0, 0)
mocker.patch('myapp.auth.datetime').now.return_value = fixed_time
token = create_token(user_id=1, expires_in_hours=24)
assert token.expires_at == datetime(2025, 1, 16, 12, 0, 0)
var emailService = new Mock<IEmailService>();
emailService
.Setup(s => s.SendAsync(It.IsAny<Email>()))
.ReturnsAsync(true);
var userService = new UserService(emailService.Object);
await userService.RegisterAsync(new RegisterRequest { Email = "alice@example.com" });
// Verify it was called
emailService.Verify(
s => s.SendAsync(It.Is<Email>(e => e.To == "alice@example.com")),
Times.Once
);
// Simulate failure
emailService
.Setup(s => s.SendAsync(It.IsAny<Email>()))
.ThrowsAsync(new SmtpException("Connection refused"));

Signs of over-mocking:

  • Tests break when you refactor but behaviour is unchanged
  • Tests are hard to understand because of all the setup
  • You’re mocking your own code (not just external dependencies)

If mocking causes pain, consider:

  • Using a real in-memory database (SQLite, in-memory Postgres)
  • Using a fake implementation (e.g., an in-memory email service)
  • Restructuring code so it’s easier to test without mocks