Test Double part 5: Understanding Mocks

abdul ahad
5 min readJan 13, 2025

--

Photo by Mel Poole on Unsplash

In software testing, Mocks are advanced test doubles designed to validate how a dependency is used by the system under test (SUT). Unlike simpler test doubles (e.g., stubs or spies), mocks involve predefining expectations about their usage and verifying those expectations after the test is executed.

The word Mock can in general also be used as a substitute for the word Test Double. let’s look at all the ways normally people use the word Mock

1. Mocking as a generic term for Test Doubles

When developers say, “I’m mocking the network stack,” they mean they’re replacing the real network logic with a simulated version (a test double). This ensures:

  1. Predictability: Tests produce consistent results.
  2. Isolation: Tests don’t depend on external systems like the internet.
  3. Speed: Avoids slow or flaky dependencies.

Mocking is a testing technique where we replace a real dependency or behavior with a simulated version (a Test Double) to make tests more predictable and independent of external factors like network requests or database queries.

Test doubles are generic terms for objects used to replace real objects in tests. There are different types of test doubles, including:

Stub

  • Provides canned answers to method calls made during a test.
  • Example: Returning predefined data when a network call is simulated.

Spy

  • Records how methods were called so you can verify interactions later.
  • Example: Checking if a method was called with specific arguments.

Mock

  • Sets expectations upfront about how methods should be called and verifies if those expectations were met.
  • Example: Defining that a specific method should be called once with certain arguments.

We will get into more detail later about a specific test double called Mock, but the gist is that when we say we are mocking something, that implies we are replacing a real dependency with a simulated one.

Example: Replacing the Network Stack

Imagine we’re testing a RemoteFeedLoader that fetches data from a network.

Without Mocking: The test depends on a real network call, which:

  • Might fail due to connection issues.
  • Takes time, making tests slower.
  • Produces unpredictable results.
let url = URL(string: "https://example.com/feed")!
let sut = RemoteFeedLoader(url: url)

sut.load { result in
// Test logic here
}

With Mocking: We replace the network stack with a test double that returns predefined results. (using stubs as test double here )

class HTTPClientStub: HTTPClient {
var requestedURL: URL?
var stubbedResult: Result<(Data, HTTPURLResponse), Error>?

func get(from url: URL, completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
requestedURL = url
if let stubbedResult = stubbedResult {
completion(stubbedResult)
}
}
}

// Test
func test_load_deliversFeedOnSuccess() {
let url = URL(string: "https://example.com/feed")!
let clientStub = HTTPClientStub()
clientStub.stubbedResult = .success((makeFeedData(), makeHTTPResponse()))
let sut = RemoteFeedLoader(client: clientStub, url: url)
sut.load { result in
switch result {
case let .success(feed):
XCTAssertEqual(feed, expectedFeed)
case .failure:
XCTFail("Expected success, got failure instead")
}
}
}

2. Mocking as a Specific Test Double

What is a Mock?

A Mock is a test double that:

  1. Sets Expectations: Before the test, you specify how the mock should be used (e.g., method calls, parameters, invocation counts).
  2. Captures Events: During the test, the mock tracks interactions with it, such as method calls or parameter values.
  3. Verifies Expectations: After the test, the mock ensures that all predefined expectations were met.

Mocks are useful for testing interaction-based behavior, ensuring that certain operations were performed in the correct way and order.

You’d usually use a Mock framework instead of writing your own because they’re really complex to create from scratch.

Example: Using a Mock

Let’s mock the HTTPClient to ensure it’s called with the correct URL.

class HTTPClientMock: HTTPClient {
private var expectations: [(methodCall: MethodCall, times: Int)] = []
private var callCounts: [MethodCall: Int] = [:]

struct MethodCall: Equatable {
let method: String
let parameters: [AnyHashable: Any]
}

func expect(_ methodCall: MethodCall, times: Int) {
expectations.append((methodCall, times))
}
func verify() {
for (expectedCall, times) in expectations {
let count = callCounts[expectedCall] ?? 0
XCTAssertEqual(count, times, "Expected \(times) calls to \(expectedCall), but got \(count)")
}
}
// Simulate a GET method
func get(from url: URL, completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
let call = MethodCall(method: "get", parameters: ["url": url])
callCounts[call, default: 0] += 1
}
}

// Test
func test_load_callsHTTPClientWithCorrectURL() {
//given
let url = URL(string: "https://example.com/feed")!
let clientMock = HTTPClientMock()
clientMock.expect(.init(method: "get", parameters: ["url": url]), times: 1)
let sut = RemoteFeedLoader(client: clientMock, url: url)

//when
sut.load { _ in }

//then
clientMock.verify()
}

1. Given:

A HTTPClientMock is created, representing the dependency being mocked.

Expectations are set on the mock using mock.expect(...). This could involve specifying:

  • Which methods should be called.
  • How many times they should be called.
  • The order in which they should be called.
  • What parameters should be passed.

2. When:

  • The system under test (sut) performs an action (sut.load { _ in }) that interacts with the mock. The mock tracks these interactions.

3. Then:

  • The mock.verify() method is called to ensure:
  • All expectations were met.
  • No unexpected interactions occurred.
  • Any specified order of operations was followed.

Why Are Mocks Complex?

  1. Expectation Setup: You need to define detailed expectations for every interaction before the test.
  2. Verification Logic: Mocks must include sophisticated mechanisms to verify that interactions match expectations.
  3. Order Validation: Validating the order of method calls adds complexity to both the setup and verification phases.
  4. High Maintenance: Changes in code behavior often require updates to mock expectations, leading to brittle tests.

This complexity makes mocks harder to write, maintain, and debug, especially when compared to simpler test doubles like stubs or spies.

Why Avoid Mocks?

  • Mocks Are Overkill for Most Scenarios: Interaction-based testing is often unnecessary; you can focus on output-based testing instead.
  • Mocks Can Be Brittle: Small changes in the code can break tests, requiring frequent updates to mock expectations.
  • Mocks Are Hard to Create: Building a reliable mock from scratch involves intricate logic for tracking and verifying interactions.

Alternatives: Stubs and Spies

Instead of mocks, simpler test doubles like stubs and spies are usually sufficient:

  1. Stubs: Return predefined responses to simulate behavior without validating interactions.
  2. Spies: Capture and record interactions, enabling you to verify usage after the fact without requiring upfront expectations.

These approaches are:

  • Easier to implement.
  • Less brittle.
  • More focused on the behavior being tested.

Summary

  • Mocks are sophisticated test doubles used to validate interactions with the SUT by setting expectations, capturing events, and verifying those expectations.
  • While they are powerful for interaction-based testing, mocks are often too complex and fragile for practical use.
  • Simpler test doubles like stubs and spies are usually sufficient for most testing needs and are easier to implement and maintain.
  • For complex mocking requirements, use a dedicated mocking framework to avoid building one from scratch

References:

--

--

abdul ahad
abdul ahad

Written by abdul ahad

A software developer dreaming to reach the top and also passionate about sports and language learning

No responses yet