Skip to content
Mocking in Tests: Complete Guide

Mocking in Tests: Complete Guide

DodaTech Updated Jun 19, 2026 6 min read

Mocking is the technique of replacing real objects in your tests with simulated versions that behave predictably — letting you test a unit of code in isolation without relying on databases, APIs, file systems, or other external dependencies.

What You’ll Learn

  • What mocking is and when (and when not) to use it
  • Python unittest.mock patterns: Mock, patch, MagicMock
  • JavaScript Jest mocks: jest.fn(), jest.spyOn(), jest.mock()
  • Spies, stubs, and fakes — the different types of test doubles
  • Best practices to avoid over-mocking

Why Mocking Matters

Without mocking, a test for a service layer would need a real database, a real API endpoint, and a real file system. These dependencies make tests slow, flaky, and hard to set up. Mocking replaces them with lightweight, fast, predictable stand-ins. A well-mocked test suite runs in seconds, not minutes.

Durga Antivirus Pro uses extensive mocking in its scanner tests — replacing the real virus definition database with mock data so tests run quickly and don’t depend on database availability.

Learning Path

    flowchart LR
  A[Unit Testing Basics] --> B[When to Mock]
  B --> C[Python unittest.mock<br/>You are here]
  B --> D[Jest Mocks]
  C --> E[Spies & Stubs]
  D --> E
  style C fill:#f90,color:#fff
  style D fill:#f90,color:#fff
  

When to Mock (and When Not To)

Mock when:

  • The dependency makes network calls (database, API, file system)
  • The dependency is slow or unreliable
  • The dependency is hard to set up (third-party auth, payment gateway)
  • You need to test error conditions that are hard to trigger

Don’t mock when:

  • The dependency is a simple value object or pure function
  • The test becomes brittle (testing implementation details)
  • You find yourself mocking everything — consider refactoring instead

Python: unittest.mock

Basic Mock

from unittest.mock import Mock, patch
import pytest

# Creating a mock object
mock_db = Mock()
mock_db.query.return_value = [{"id": 1, "name": "Alice"}]

# Using the mock
result = mock_db.query("SELECT * FROM users")
assert result == [{"id": 1, "name": "Alice"}]
mock_db.query.assert_called_once_with("SELECT * FROM users")

Patch Decorator

# service.py
import requests

def get_user(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    response.raise_for_status()
    return response.json()

# test_service.py
from unittest.mock import patch
from service import get_user

@patch("service.requests.get")
def test_get_user(mock_get):
    # Arrange
    mock_response = Mock()
    mock_response.json.return_value = {"id": 1, "name": "Alice"}
    mock_response.raise_for_status.return_value = None
    mock_get.return_value = mock_response

    # Act
    result = get_user(1)

    # Assert
    assert result == {"id": 1, "name": "Alice"}
    mock_get.assert_called_once_with("https://api.example.com/users/1")

Multiple Patches

@patch("service.email_service")
@patch("service.user_repository")
def test_register_user(mock_repo, mock_email):
    mock_repo.find_by_email.return_value = None
    mock_repo.save.return_value = True

    result = register_user("alice@example.com", "password123")

    assert result.success is True
    mock_repo.save.assert_called_once()
    mock_email.send_welcome.assert_called_once()

Side Effects (Testing Errors)

import requests.exceptions

@patch("service.requests.get")
def test_get_user_network_error(mock_get):
    mock_get.side_effect = requests.exceptions.ConnectionError("Network error")

    with pytest.raises(requests.exceptions.ConnectionError):
        get_user(1)

# Alternating return values
mock_db.query.side_effect = [
    [{"id": 1}],  # First call
    [{"id": 2}],  # Second call
    [],           # Third call
]

JavaScript: Jest

jest.fn() — Basic Mock

// Basic mock function
const mockFn = jest.fn();
mockFn.mockReturnValue(42);

console.log(mockFn()); // 42
expect(mockFn).toHaveBeenCalledTimes(1);

// With implementation
const add = jest.fn((a, b) => a + b);
expect(add(2, 3)).toBe(5);
expect(add).toHaveBeenCalledWith(2, 3);

jest.spyOn() — Spying on Existing Methods

// userService.js
export const userService = {
  async getUser(id) {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
  },
};

// userService.test.js
import { userService } from './userService';

describe('getUser', () => {
  it('returns user data', async () => {
    const mockUser = { id: 1, name: 'Alice' };

    // Spy on fetch and mock its return
    jest.spyOn(global, 'fetch').mockResolvedValue({
      json: async () => mockUser,
      ok: true,
    });

    const result = await userService.getUser(1);

    expect(result).toEqual(mockUser);
    expect(fetch).toHaveBeenCalledWith('/api/users/1');

    // Clean up
    jest.restoreAllMocks();
  });
});

jest.mock() — Module Mocking

// database.js
export const db = {
  async query(sql, params) {
    // Real database call
  },
};

// userRepository.js
import { db } from './database';

export async function findUserByEmail(email) {
  const result = await db.query('SELECT * FROM users WHERE email = $1', [email]);
  return result.rows[0];
}

// userRepository.test.js
jest.mock('./database', () => ({
  db: {
    query: jest.fn(),
  },
}));

import { findUserByEmail } from './userRepository';
import { db } from './database';

test('finds user by email', async () => {
  db.query.mockResolvedValue({
    rows: [{ id: 1, email: 'alice@example.com', name: 'Alice' }],
  });

  const user = await findUserByEmail('alice@example.com');

  expect(user.name).toBe('Alice');
  expect(db.query).toHaveBeenCalledWith(
    'SELECT * FROM users WHERE email = $1',
    ['alice@example.com']
  );
});

Spies — Verify Calls Without Changing Behavior

const logger = { info: (msg) => console.log(msg) };
const spy = jest.spyOn(logger, 'info');

processOrder(order);

expect(spy).toHaveBeenCalledWith('Order processed: 123');
// Original logger.info still works

spy.mockRestore(); // Restore original

Test Double Types

TypePurposeExample
StubReturns fixed valuesmock.return_value = 42
SpyRecords calls without changing behaviorjest.spyOn(obj, 'method')
MockPre-programmed with expectationsmock.assert_called_once_with(x)
FakeLightweight working implementationIn-memory database instead of real one

Common Errors

1. Over-Mocking

Mocking everything makes tests brittle — they break when implementation details change. Mock boundaries (external APIs, databases) but not internals.

2. Not Restoring Mocks

If a mock isn’t restored, it leaks to other tests. Use patch as a decorator/context manager or call mock.restore() in afterEach.

3. Mocking the Wrong Thing

Patch the import path where the function is used, not where it’s defined. @patch("service.requests.get") patches requests.get in service.py, not requests.py.

4. Asserting Too Much

Only assert what’s necessary for the behavior under test. Over-asserting makes tests fragile.

5. Mocking Value Objects

Don’t mock data classes, DTOs, or config objects. Use real instances.

6. Forgetting to Test Error Paths

Mock side_effect to test how your code handles network errors, timeouts, and invalid responses.

Practice Questions

  1. What is a mock? A simulated object that replaces a real dependency, returning pre-programmed values and recording how it was called.

  2. When should you NOT mock? When the dependency is a simple value object, pure function, or when mocking makes the test brittle.

  3. How do you patch an external API call in Python? Use @patch("module.requests.get") — patch at the import location where it’s used.

  4. What’s the difference between a stub and a mock? A stub returns fixed values. A mock has expectations about how it should be called and asserts against those expectations.

  5. What does jest.spyOn do? Creates a spy that wraps an existing method, recording calls without changing the original behavior.

Challenge: Write a test suite for a service that fetches user data from an API, stores it in a database, and sends a welcome email. Mock all three external dependencies. Test the success path and two error paths (API failure, database failure).

FAQ

Does mocking make tests less valuable?
If used correctly, no. Mocking isolates the unit under test so you know exactly what failed. If a real database test fails, it could be the code, the database, the network, or the test data.
Should I use real databases in tests?
Integration tests against a real database are valuable. But they’re slower and require setup. Use them sparingly for critical paths; use mocks for most unit tests.
What is monkey patching?
Replacing attributes or methods at runtime. Python’s patch is a form of monkey patching with cleanup support.
Can I mock imported modules?
Yes. In Python, @patch("module.ClassName"). In Jest, jest.mock("module-name").
How do I mock environment variables?
In Python, use monkeypatch.setenv (pytest) or @patch.dict(os.environ, ...). In Jest, set process.env.VAR = 'value' in the test.

What’s Next

TutorialWhat You’ll Learn
Jest Testing FrameworkComplete Jest guide with advanced patterns
Integration Testing GuideTesting with real dependencies — the alternative to mocking
Unit Testing Best PracticesFoundation concepts for effective testing

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-19.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro