3 August 2020

Mocks, Stubs and Fakes in Swift Unit Tests


Exploring the difference between mocks, stubs and fakes in testing.


Mocking is a technique used often in unit testing, to decouple the code being tested from its dependencies or collaborators. The term “mock” is often used interchangeably when referring to different types of mock objects. This sometimes leads to confusion when it comes to naming the mock objects, or discussions that arise between developers when discussing the usage of the mock in question. The purpose of this post is to try to get the terminology right, highlight the different types of mocks that are available, and when to use them.

Before we dive in, we need to understand the different types of unit tests that exist, which will also help to lay the foundation for when to choose the appropriate mocking strategy.

State versus behavioural tests

To start off, let’s take a look at the difference between state and behavioural tests. With state verification, we determine whether an exercised method worked correctly by examining the state of the system / service under test (SUT) and its collaborators after the method was exercised. With behavioural tests on the other hand, we determine whether the exercised method interacts with the components which it depends (collaborators). With behavioural verification, we test that the SUT is behaving as expected rather than just ending up in the correct post-exercise state.

What is mocking?

In unit testing, mocking is a term used when creating objects that simulate the behaviour of real objects. An object under test may have dependencies on other objects, which might in turn be complex objects. To isolate the behaviour of the object we want to test, we replace the other objects with mocks (also known as test doubles) that simulate the behaviour of the real objects. This is useful if the real objects are impractical to incorporate into the unit test.

Mock Object Pattern

When writing unit tests, we prefer to test one behaviour at a time. Tests which test many behaviours at once tend to be complex, and do not give very valuable information when they fail. The Mock Object Pattern isolates the class being tested from an entity it depends upon, breaking the dependency, and in doing so making the dependant object more easily testable. 

A test should be written in such a way that it has only a single reason to fail. If we do not break dependencies, then a test can fail when one or more of the dependencies fail, even though the thing we want to test is working fine.

A well-designed test suite should have tests that run fast, so that it can be run frequently. A test with heavyweight dependencies cannot run fast. We should aim to keep tests as simple as possible, but a test that requires a lot of objects to be instantiated will tend to be complex.

Mocks, stubs, and fakes - what's the difference?

When it comes to mocking, there are a variety of options to choose from. Although what follows is not an exhaustive list of the available options, they are the most commonly used in my experience.

Mocks
Mocks are objects that are pre-programmed with expectations which form a specification of the calls they are expected to receive.

Below is a test suite for a class called HealthActivityTrackingViewModel. It contains a single test that checks that an expected list of tracked items is loaded when serviceUnderTest.loadTrackedItems() is called. The test verifies that the list of tracked items is initially nil, and then contains the expected tracked items after the method is exercised:

class HealthActivityTrackingViewModel: XCTestCase {
    
    private var serviceUnderTest: HealthActivityTrackingViewModel!
    
    override func setUp() {
        super.setUp()
        
        serviceUnderTest = HealthActivityTrackingViewModel(navigationController: mockNavigationController)
    }
    
    override func tearDown() {
        serviceUnderTest = nil
        
        super.tearDown()
    }
    
    func testUserHasExpectedTrackedItemsAfterLoadTrackedItemsIsInvoked() {
        XCTAssertNil(serviceUnderTest.trackedItems)
        
        serviceUnderTest.loadTrackedItems()
        
        let actual = serviceUnderTest.trackedItems
        let expected = ["Exercise", "Sleep"]
        
        XCTAssertNotNil(actual)
        XCTAssertEqual(expected, serviceUnderTest.trackedItems, "The expected health tracked items were not loaded: actual: \(String(describing: actual))")
    }
}

This is an example of a state verification test. The view model under test can be viewed below:

public class HealthActivityTrackingViewModel {
    private let navigationController: UINavigationController!
    private var user = User()
    public private(set) var trackedItems: [String]?
    
    public init() {
        self.navigationController = UINavigationController()
    }
    
    public init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    public func loadTrackedItems() {
        trackedItems = user.trackedItems(for: "Health")
    }
    
    public func performNavigation() {
        if user.hasExerciseTrackingEnabled() {
            navigationController.present(ExerciseTrackingViewController(), animated: true, completion: nil)
        } else {
            if user.hasSleepTrackingEnabled() {
                navigationController.present(SleepTrackingViewController(), animated: true, completion: nil)
            } else {
                navigationController.present(HealthActivitySetupViewController(), animated: true, completion: nil)
            }
        }
    }
}

In addition to the loadTrackedItems() method, the view model also exposes a method called performNavigation(), that uses a UINavigationController to navigate to a view controller based on the state of a User object. This method does not change the state of the object, but it does contain behaviour.

It is obvious from the code snippet above that the view model has two dependencies that are used in the performNavigation() method. To put this method under test, the easiest route is to test the branch where the navigation controller presents the HealthActivitySetupViewController. Because this method does not mutate state, but performs navigation, we need to write a behavioural test. This is a perfect candidate for a mock object that we can use to verify that the expected behaviour took place during the test. What we want to do is verify that a navigation controller presented the expected view controller. We accomplish this by creating a subclass of UINavigationController, and override the present(...) method:

public class MockNavigationController: UINavigationController  {
    private var destinationViewController: UIViewController!
    
    override public func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        destinationViewController = viewControllerToPresent
    }
}

public protocol MockNavigationControllable {
    func resetForTesting()
}

extension MockNavigationController: MockNavigationControllable {
    public func resetForTesting() {
        destinationViewController = nil
    }
    
    public func didNavigateToHealthActivitySetupViewController() -> Bool {
        return destinationViewController is HealthActivitySetupViewController
    }
}

In the mock we record the view controller that the navigation controller was asked to present. The mock also exposes a method to verify that the view controller that was presented is the Health Activity Setup view controller.

To test this behaviour we create an instance of the mock navigation controller in the test suite, and write a test that exercises the performNavigation() method:

class HealthActivityTrackingViewModel: XCTestCase {
    
    private var serviceUnderTest: HealthActivityTrackingViewModel!
    private let mockNavigationController = MockNavigationController()
    
    override func setUp() {
        super.setUp()
        
        serviceUnderTest = HealthActivityTrackingViewModel(navigationController: mockNavigationController)
    }
    
    override func tearDown() {
        serviceUnderTest = nil
        
        mockNavigationController.resetForTesting()
        
        super.tearDown()
    }
    
    func testUserHasExpectedTrackedItemsAfterLoadTrackedItemsIsInvoked() {
        XCTAssertNil(serviceUnderTest.trackedItems)
        
        serviceUnderTest.loadTrackedItems()
        
        let actual = serviceUnderTest.trackedItems
        let expected = ["Exercise", "Sleep"]
        
        XCTAssertNotNil(actual)
        XCTAssertEqual(expected, serviceUnderTest.trackedItems, "The expected health tracked items were not loaded: actual: \(String(describing: actual))")
    }
    
    func testUserIsNavigatedToHealthActivitySetupIfHasExerciseTrackingEnabled() {
        serviceUnderTest.performNavigation()
        
        XCTAssertTrue(mockNavigationController.didNavigateToHealthActivitySetupViewController(), "User without Exercise Tracking enabled should be navigated to Health Activity Setup view controller")
    }
}

In the test we use the ask the mock navigation controller to verify that the Health Activity Setup view controller was presented after the performNavigation() method was exercised on the SUT.

Stubs
Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.

In our test suite for the HealthActivityTrackingViewModel class, only one branch of the performNavigation() method is currently exercised, where a mock navigation controller is used to verify that the expected view controller was navigated to. However, there are two other branches that are not under test. They require that a User instance be in a state that either has exercise or sleep activity tracking enabled.

One option to get the dependency in the required state for the test would be to load a User instance from Core Data, and then set a number of properties on that instance before calling the performNavigation() method on the SUT. The alternative is to use a class of mock object known as a stub.

With that said, the next test to write would be for the scenario where the current user has exercise tracking enabled. We do so as follows:

func testUserIsNavigatedToExerciseTrackingViewControllerIfHasExerciseTrackingEnabled() {
    // Arrange / Build
    serviceUnderTest.setUser(UserWithExerciseTrackingEnabledStub())
        
    // Act / Operate
    serviceUnderTest.performNavigation()
        
    // Assert / Check
    XCTAssertTrue(mockNavigationController.didNavigateToExerciseTrackingViewController(), "User with exercise tracking enabled should be navigated to Exercise Tracking view controller")
}

In this test we use the mock navigation controller again to verify that the expected view controller was presented, which in this case should be the Exercise Tracking view controller. What has been introduced in this test however is the use of a stub called UserWithExerciseTrackingEnabledStub. The stub is defined as such:

public class UserWithExerciseTrackingEnabledStub: User {
    override public func hasExerciseTrackingEnabled() -> Bool {
        return true
    }
}

It is a subclass of User, and overrides the hasExerciseTrackingEnabled()method of its superclass, which simply returns true when asked whether the user has exercise tracking enabled.

What should be noted here is that UserWithExerciseTrackingEnabledStub is not being used to verify any behaviour as a true mock object would do. Instead, it simply provides an answer to a question that was asked during a unit test.

So that leaves us with one more branch of the performNavigation() method that needs to be put under test, which is for the scenario where the current user does not have exercise tracking enabled, but does have sleep tracking enabled:

func testUserIsNavigatedToSleepTrackingViewControllerIfHasSleepTrackingEnabledButNotExerciseTrackingEnabled() {
    serviceUnderTest.setUser(UserWithSleepTrackingEnabledStub())
    
    serviceUnderTest.performNavigation()
        
    XCTAssertTrue(mockNavigationController.didNavigateToSleepTrackingViewController(), "User with exercise tracking disabled but sleep tracking enabled should be navigated to Sleep Tracking view controller")
}

Here we have used the stub approach again, this time passing in a User stub that responds with true when asked if it has sleep tracking enabled:
 
public class UserWithSleepTrackingEnabledStub: User {
    override public func hasSleepTrackingEnabled() -> Bool {
        return true
    }
}

Fakes
Fakes are a class of mock object that take shortcuts over things like databases and assertions. Fake objects can actually have working implementations, but usually take some shortcut which makes them not suitable for production (an in-memory database is a good example).

In the following example, we have a Swift class (SomeClass) that has a dependency on a Configurator class. SomeClass invokes a method on the Configurator class, which then sets a private property (isConfigured) to true after the configuration operation completes.

class SomeClass {    
  private var configurator = Configurator()    
  private(set) var isConfigured = false

  func load() {        
    configurator.configure()        
    isConfigured = true    
  }
}

class Configurator {    
  func configure() {        
    assertionFailure("Crashing the app to demonstrate code that should not be executed in a test")
  }
}

We have a test suite for SomeClass that exercises the load() method, and then asserts that the isConfigured property is set to true after the operation completes:

class SomeClassTests: XCTestCase {        
  
  func testLoadPerformsConfiguration() {        
    // Arrange
    let serviceUnderTest = SomeClass()        
    
    // Act
    serviceUnderTest.load()                
    
    // Assert
    XCTAssertTrue(serviceUnderTest.isConfigured)    
  }
}

As you might expect, when testLoadPerformsConfiguration() is run, the test suite crashes. This is because there is an assertionFailure in the configure()method. This is an example of a scenario where a configuration file is perhaps loaded that is only available in a production environment, or a call is made to a database that is not available in the test environment. We would like to test that the isConfigured property is set to true after the configure() method on the Configurator dependency is invoked, but the problem is that an assertion is thrown in the dependency that we might not have any control over. This is where a fake object comes in handy.

Below is a fake Configurator class, which overrides the configure() method of the super class, and simply prints a message to the console:

class ConfiguratorFake: Configurator {    
  override func configure() {        
    print("************ Fake \(#function) was invoked")    
  }
}

We pass this fake configurator to the service under test, which allows us to take a shortcut over Configurator.configure() and the assertionFailure in the dependency:

class SomeClassTests: XCTestCase {        
  func testLoadPerformsConfiguration() {        
    let serviceUnderTest = SomeClass()        
    serviceUnderTest.setConfigurator(ConfiguratorFake())                
    
    serviceUnderTest.load()                
    
    XCTAssertTrue(serviceUnderTest.isConfigured)    
  }
}

This means we can run our test suite without it crashing by mocking out the Configurator dependency, and test the state change of the class we are testing.

At this point it might be useful to explain why ConfiguratorFake is a fake, and not a mock or a stub. We are not using ConfiguratorFake to verify any expectations during a test as we would do with a mock, nor are we using it to provide a canned answer during a test as we would with a stub. We are simply using it to take a shortcut over some code so that we can test the state of the SUT, and not its collaborators. 

In Summary

  • State tests determine whether an exercised method worked correctly by examining the state of the SUT and its collaborators after the method was exercised.
  • Behavioural tests determine whether the exercised method interacts with the components on which it depends (collaborators) as expected.
  • Mocks verify predefined expectations.
  • Stubs provide canned answers to calls made during the test.
  • Fakes take shortcuts over things like databases and production assertions.