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
ViewModelor 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.

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
StubNetworkServicethat always returns a specificUserobject whenfetchUser()is called, allowing you to test how yourViewModelformats the user's name for display.
- Example: A
-
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
FakeUserDefaultsthat uses an in-memoryDictionaryinstead of writing to disk. It correctly stores and retrieves values, but it's much faster and doesn't have side effects.
- Example: A
-
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
MockAnalyticsServicethat you use to verify that callinglogin()on yourViewModelalso results in a single call toanalytics.logEvent(name: \"user_login\").
- Example: A
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.
- Define a Protocol: Create a protocol that describes the services the dependency provides.
protocol DiscountEligibilityCalculating { func getDiscountEligibility(for flight: Flight) -> Bool } - Conform to the Protocol: Make your real object conform to this protocol.
class FlightsDiscountEligibilityCalculator: DiscountEligibilityCalculating { // ... implementation } - Inject the Dependency: The
ViewModelnow 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:
- Arrange:
- Create an instance of your mock object (
DummyEligibilityCalculator). You can configure it to returntrueorfalseto test different scenarios. - Create an instance of your
ViewModel(the SUT), injecting the mock object.
- Create an instance of your mock object (
- Act: Call the method on the
ViewModelthat you want to test. - 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.