Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Stubs, Fakes, and Mocks: Isolating Dependencies with Mock Objects

Recommended resources

Welcome back! In our previous lesson, we established a solid foundation for unit testing. You learned how to structure tests using the Arrange-Act-Assert pattern, write assertions to verify outcomes, and use setUp and tearDown to ensure tests run in isolation. We focused on a simple ViewModel with no external dependencies.

Today, we take the next logical step. Most objects in a real-world application don't live in a vacuum; they collaborate with other objects—network services, databases, analytics trackers—to get their job done. To truly test a unit in isolation, we must learn to manage these dependencies.

Our goal for this lesson is to differentiate between stubs, fakes, and mocks, and create a mock object to isolate a dependency. Understanding these "test doubles" is a crucial skill that unlocks the ability to test complex business logic reliably and efficiently.

The Problem: Untestable Dependencies

Imagine your ViewModel needs to fetch user data from a server. If your unit test calls the real network service, it introduces several problems:

  • Slow: Network requests take time, and a suite of slow tests is a suite that developers won't run often.
  • Flaky: The test could fail due to network issues or a staging server being down, not because your logic is wrong.
  • Side Effects: The test might create or modify real data on the backend, which is undesirable.
  • Lack of Isolation: If the test fails, is the bug in your ViewModel or in the network layer? It's hard to tell.

To write a unit test, we must isolate the "unit under test" (SUT) from its dependencies. We achieve this by replacing the real dependencies with stand-ins called Test Doubles.

Mocks vs. Fakes vs. Stubs | Mocking in Java | Mock Frameworks | Geekific

The video 'Mocks vs. Fakes vs. Stubs' from the Geekific channel provides an excellent, language-agnostic overview of why we need to replace dependencies in our tests.

Watch the first three minutes (00:03 to 03:18). Focus on the core problem: how dependencies on external systems like databases or web services make true unit testing difficult, and how mock objects provide a solution.


The Solution: A Family of Test Doubles

"Test Double" is the general term for any object that stands in for a real dependency during a test. While people often use the term "mock" loosely to refer to all of them, there are specific types with distinct purposes. Understanding the difference will make your tests clearer and more effective.

Venn Diagram of Test Doubles
This Venn diagram shows the relationships between the common types of test doubles. Fakes can be seen as a type of Stub, and Spies as a type of Mock. Today we will focus on the key distinctions between Stubs, Fakes, and Mocks.

Let's define the main players.

Mocks vs. Fakes vs. Stubs | Mocking in Java | Mock Frameworks | Geekific

Let's return to the Geekific video for a conceptual breakdown of Fakes, Stubs, and Mocks.

Watch the segment from 05:28 to 07:51. This part clearly explains the different goals of each test double.

To summarize and put it into context:

  • Stub: Provides pre-programmed, "canned" answers to method calls. Its purpose is to control the state of the SUT. You use a stub when your test needs a dependency to return specific data, like a successful result or a particular error.

    • Example: A StubNetworkService that always returns a specific User object when fetchUser() is called, allowing you to test how your ViewModel formats the user's name for display.
  • Fake: A working, but simplified, implementation of the dependency. Unlike a stub, it's not just returning canned data; it has real logic. It's useful for replacing heavy dependencies like a database with a lightweight alternative.

    • Example: A FakeUserDefaults that uses an in-memory Dictionary instead of writing to disk. It correctly stores and retrieves values, but it's much faster and doesn't have side effects.
  • Mock: A "smart" object that you configure with expectations about how it will be used. The focus of a mock is behavior verification. You use a mock to assert that your SUT interacted with its dependency in the correct way.

    • Example: A MockAnalyticsService that you use to verify that calling login() on your ViewModel also results in a single call to analytics.logEvent(name: \"user_login\").

The primary distinction is State vs. Behavior Verification:

  • Use Stubs and Fakes when you care about the state they provide to your SUT.
  • Use Mocks when you care about the behavior of your SUT—that is, how it interacts with its dependency.

For a deeper dive with Swift-specific code, the following article provides clear, concise examples of each type of test double.

Swift Test Doubles Spy Stub Dummy Fake Mock

The article 'Swift Test Doubles Spy Stub Dummy Fake Mock' on mobidevtalk.com demonstrates how to implement each type of test double in Swift. It will help solidify the concepts with practical code.

Read the sections titled 'Stub', 'Fake', and 'Mock'. For each one, examine the Swift code implementation and see how it aligns with the definitions we just covered. You can skim the 'Dummy' and 'Spy' sections, as they are less common or are variations of the main three.


Implementing a Mock: Dependency Injection

You now know what a mock is and why you'd use one. But how do you actually substitute a real object for a mock in your test? You can't if your code looks like this:

class FlightDetailsViewModel {
    private let discountCalculator = FlightsDiscountEligibilityCalculator() // Hard-coded dependency

    func calculateDiscount() {
        // ... uses the discountCalculator
    }
}

Here, FlightDetailsViewModel creates its own FlightsDiscountEligibilityCalculator. There is no way for a test to replace it with a mock.

The solution is Dependency Injection (DI). Instead of letting the ViewModel create its dependencies, we "inject" them from the outside, typically through the initializer. To make this truly flexible, we depend on an abstraction (a protocol) rather than a concrete class.

  1. Define a Protocol: Create a protocol that describes the services the dependency provides.
    protocol DiscountEligibilityCalculating {
        func getDiscountEligibility(for flight: Flight) -> Bool
    }
    
  2. Conform to the Protocol: Make your real object conform to this protocol.
    class FlightsDiscountEligibilityCalculator: DiscountEligibilityCalculating {
        // ... implementation
    }
    
  3. Inject the Dependency: The ViewModel now holds a reference to any object that conforms to the protocol.
    class FlightDetailsViewModel {
        private let discountCalculator: DiscountEligibilityCalculating // Depend on the protocol!
    
        init(discountCalculator: DiscountEligibilityCalculating) { // Inject via init
            self.discountCalculator = discountCalculator
        }
        // ...
    }
    

With this pattern in place, you can pass the real FlightsDiscountEligibilityCalculator in your app, and a mock version in your tests.

This next video demonstrates this exact process, showing how to refactor a ViewModel to be testable by using a protocol and dependency injection.

MVVM + Dependency Injection in Swift | Unit Testing | iOS

The video 'MVVM + Dependency Injection in Swift' by iCode provides a practical walkthrough of making a ViewModel testable.

Please watch from 22:47 to 26:36. The first part (22:47 - 24:48) explains why creating a protocol is essential for breaking the hard dependency. The second part (25:28 - 26:36) shows the creation of a 'dummy' class (our test double) that conforms to this protocol, enabling isolated testing.

As you saw in the video, once the dependency is injected, the test setup becomes straightforward:

  1. Arrange:
    • Create an instance of your mock object (DummyEligibilityCalculator). You can configure it to return true or false to test different scenarios.
    • Create an instance of your ViewModel (the SUT), injecting the mock object.
  2. Act: Call the method on the ViewModel that you want to test.
  3. Assert: Check that the ViewModel's properties have been updated correctly based on the canned response from your mock.

This approach gives you full control over the ViewModel's environment, allowing for fast, reliable, and isolated tests.


Conclusion

Today you've tackled one of the most important concepts in modern software testing. By learning to manage dependencies with test doubles, you've unlocked the ability to unit test the vast majority of your application's logic.

Key Takeaways:

  • Directly using real dependencies like network services or databases in unit tests makes them slow, unreliable, and hard to debug.
  • Test Doubles are stand-ins for real dependencies. The main types are Stubs, Fakes, and Mocks.
  • Stubs provide canned responses to control the state of the system under test.
  • Fakes are lightweight, working implementations of a dependency (e.g., an in-memory database).
  • Mocks are used to verify behavior—asserting that the system under test interacts with its dependencies as expected.
  • Dependency Injection via protocols is the key architectural pattern that enables you to replace real dependencies with test doubles.

Preview of the Next Lesson:
We've successfully tested logic that depends on synchronous services. But what about asynchronous operations, like a network request that takes time to complete? In our next lesson, you will learn how to test asynchronous code within a ViewModel using XCTestExpectation, ensuring your tests can handle code that doesn't execute immediately.