You are currently viewing Test Doubles in Swift: Dummy, Fake, Stub, Mock

Test Doubles in Swift: Dummy, Fake, Stub, Mock

When doing unit testing, it is a common practice to replace an actual object with a simplified version in order to reduce code dependencies. We call this kind of simplified object a Test Double (similar to stunt double in the movie industry).

By using a test double, we can highly reduce the complexity of our test cases. Furthermore, it also enables us to have more control over the outcome of our test items.

In this article, we will dive deep into 4 types of test doubles (Dummy, Fake, Stub and Mock). We will look into the definition for each of them, what are their differences, as well as how to perform unit test with test doubles using XCTest framework.

The Example

Before we get started, let’s look at the class that we are going to test — TelevisionWarehouse.

class TelevisionWarehouse {

    private let emailServiceHelper: EmailServiceHelper
    private let databaseReader: DatabaseReader

    private let minStockCount = 3
    private var stocks: [Television]

    // Failable initializer
    // Inject dependencies: DatabaseReader, EmailServiceHelper
    // Initializer will fail when not able to read stocks information from database
    init?(_ databaseReader: DatabaseReader, emailServiceHelper: EmailServiceHelper) {

        self.emailServiceHelper = emailServiceHelper
        self.databaseReader = databaseReader

        // 1
        let result = databaseReader.getAllStock()

        switch result {
        case .success(let stocks):
            self.stocks = stocks
        case .failure(let error):
            print(error.localizedDescription)
            return nil
        }
    }
    
    var stockCount: Int {
        return stocks.count
    }

    // 2
    // Add televisions to warehouse
    func add(_ newStocks: [Television]) {
        stocks.append(contentsOf: newStocks)
    }
    
    // 3
    // Remove televisions from warehouse
    func remove(_ count: Int) {
        
        if count <= stockCount {
            stocks.removeLast(count)
        } else {
            stocks.removeAll()
        }

        // 4
        // When stock less than minimum threshold, send email to manager
        if stocks.count < minStockCount {
            emailServiceHelper.sendEmail(to: "manager@email.com")
        }
    }
}

struct Television {
    let brand: String
    let price: Double
}

The TelevisionWarehouse have 4 functionalities:

  1. Read stocks information from database using database reader.
  2. Add new stocks to warehouse.
  3. Remove stocks from warehouse.
  4. Send notification email when stock count less than the minimum threshold.

We will be writing unit test case for these functionalities in a short while.

Note that we are using dependency injection to inject both DatabaseReader and EmailServiceHelper into TelevisionWarehouse class. The DatabaseReader will be in charge of reading stocks information from database, while EmailServiceHelper will be in charge of sending out notification email.

Following code snippet shows the protocol definition and implementation skeleton for both DatabaseReader and EmailServiceHelper.

protocol DatabaseReader {
    func getAllStock() -> Result<[Television], Error>
}

class RealDatabaseReader: DatabaseReader {

    init() {
        // Make database connection
        // Perfrom nessesary configuration
        // ...
        // ...
    }

    func getAllStock() -> Result<[Television], Error> {

        // Construct database query
        // Perform database query
        // ...
        // ...
        
        if let error = error {
            // When error occurred
            return .failure(error)
        }

        // Successfully query database
        return .success(result)
    }
}
protocol EmailServiceHelper {
    func sendEmail(to address: String)
}

class RealEmailServiceHelper: EmailServiceHelper {
    func sendEmail(to address: String) {
        // Compose email content
        // Connect to email server
        // ...
        // ...
        // Send out email
    }
}

Note that the actual implementation for both of these classes are not important because we will be creating test doubles for both of these classes.

With all that in mind, let’s start testing the TelevisionWarehouse class with test doubles!


Dummy

Definition

Dummy objects are objects that are not being used in a test and only act as a placeholder. It usually does not contain any implementation.

Using Dummy in Unit Test

Let’s say we want to verify that an instance of TelevisionWarehouse can be successfully created if no error occurred, in this kind of situation the implementations for both DatabaseReader and EmailServiceHelper are not important and can be neglected.

Therefore, we can reduce the complexity of our test case by injecting a dummy instances of DatabaseReader and EmailServiceHelper into the TelevisionWarehouse initializer.

Following code shows the implementation of a dummy DatabaseReader and a dummy EmailServiceHelper.

// Dummy DatabaseReader
class DummyDatabaseReader: DatabaseReader {
    func getAllStock() -> Result<[Television], Error> {
        return .success([])
    }
}

// Dummy EmailServiceHelper
class DummyEmailServiceHelper: EmailServiceHelper {
    func sendEmail(to address: String) {}
}

With both dummies ready, we can now use it in our unit test.

func testWarehouseInitSuccess() {

    // Create dummies
    let dummyReader = DummyDatabaseReader()
    let dummyEmailService = DummyEmailServiceHelper()
    
    // Initialize TelevisionWarehouse
    let warehouse = TelevisionWarehouse(dummyReader, emailServiceHelper: dummyEmailService)
    
    // Verify warehouse init successful
    XCTAssertNotNil(warehouse)
}

Fake

Definition

Fake is an object that have actual implementations that replicate the behaviour and outcome of its original class but in a much simpler manner.

Fake objects are usually used when we want to avoid complex configurations or time consuming operations during a test. An example of this will be connecting to databases or making network requests.

Using Fake in Unit Test

To be able to test out the TelevisionWarehouse class’s add / remove stocks functionality, we must have a functioning DatabaseReader instance to load some sample data for testing purposes.

However, in most cases, we do not want to hit our production database while running the test. In this kind of situation, instead of reading data from database, we will create a fake database reader that reads data from a JSON file.

class FakeDatabaseReader: DatabaseReader {

    func getAllStock() -> Result<[Television], Error> {

        // Read JSON file
        let filePath = Bundle.main.path(forResource: "stock_sample", ofType: "json")
        let data = FileManager.default.contents(atPath: filePath!)
        
        // Parse JSON to object
        let decoder = JSONDecoder()
        let result = try! decoder.decode([Television].self, from: data!)

        return .success(result)
    }
}

// Conform Television to Decodable protocol for JSON parsing
extension Television: Decodable {
    
}

Note that stock_sample.json contains 3 television objects.

Now, let’s inject a fake database reader together with a dummy email service helper to test out the TelevisionWarehouse class’s add / remove stocks functionality.

func testWarehouseAddRemoveStock() {
    
    let fakeReader = FakeDatabaseReader()
    let dummyEmailService = DummyEmailServiceHelper()
    
    let warehouse = TelevisionWarehouse(fakeReader, emailServiceHelper: dummyEmailService)!
    
    // Add 2 televisions to warehouse
    warehouse.add([
        Television(brand: "Toshiba", price: 199),
        Television(brand: "Toshiba", price: 199)
    ])
    
    // Remove 4 televisions from warehouse
    warehouse.remove(4)
    
    // Verify stock count is correct
    XCTAssertEqual(warehouse.stockCount, 1)
    
    // Remove amount more than stock count
    warehouse.remove(100)
    
    // Verify that stock count is 0
    XCTAssertEqual(warehouse.stockCount, 0)
}

By using a fake database reader, we manage to avoid the slow process of connecting to a database. Furthermore, it is also much easier to control what data being loaded into the test.


Stub

Definition

Stub is an object where its functions will always return a set of predefined data. It is especially useful when we want to simulate certain condition that is extremely difficult to achieve in real life, such as server errors or network connection errors.

Using Stub in Unit Test

At this point, you might have noticed that the TelevisionWarehouse class have a failable initializer. The initialization will fail when the database reader returns error while reading the database.

In real life, it is quite difficult to force a database error so that we can test out the failable initializer. In this kind of situation, what we can do is to create a stub database reader that always returns an error when we call getAllStock().

class StubDatabaseReader: DatabaseReader {
    
    enum StubDatabaseReaderError: Error {
        case someError
    }

    func getAllStock() -> Result<[Television], Error> {
        return .failure(StubDatabaseReaderError.someError)
    }
}

The way to use StubDatabaseReader is fairly straightforward.

func testWarehouseInitFail() {
    
    let stubReader = StubDatabaseReader()
    let dummyEmailService = DummyEmailServiceHelper()
    
    let warehouse = TelevisionWarehouse(stubReader, emailServiceHelper: dummyEmailService)
        
    // Verify warehouse object is nil
    XCTAssertNil(warehouse)
}

Stub vs Fake

Up until this stage, you might have noticed that there are some similarities between stub and fake. In fact, you can actually achieve the same result of fake getAllStock() by creating a stub getAllStock() that returns an array of Television objects.

However, when it comes to a situation where you need to load a huge amount of data (10 thousand Television objects), then using fake is still a preferred solution.


Mock

Definition

Mock is an object that keeps track of which method being called and how many times it was called. Furthermore, you can also use a mock to inspect the behaviour and data flow of a class.

Using Mock in Unit Test

One of the functionalities of the TelevisionWarehouse class is to send out notification email when stock count less than the minimum threshold. By using a mock email service helper, we can verify the following behaviours:

  1. sendEmail(to:) is called.
  2. Only 1 email is sent (sendEmail(to:) not being called multiple times).
  3. The email’s recipient is the manager.

After knowing what we wanted to verify, let’s take a look at the mock email service helper.

class MockEmailServiceHelper: EmailServiceHelper {
    
    var sendEmailCalled = false
    var emailCounter = 0
    var emailAddress = ""

    func sendEmail(to address: String) {
        sendEmailCalled = true
        emailCounter += 1
        emailAddress = address
    }
}

With the mock email service helper ready, we can then test out the email sending behaviours.

func testWarehouseSendEmail() {
    
    // FakeDatabaseReader will load 3 televisions
    let fakeReader = FakeDatabaseReader()
    let mockEmailService = MockEmailServiceHelper()
    
    let warehouse = TelevisionWarehouse(fakeReader, emailServiceHelper: mockEmailService)!
    
    // Remove warehouse's stocks to trigger notification email
    warehouse.remove(3)
    
    // Verify sendEmail(to:) called
    XCTAssertTrue(mockEmailService.sendEmailCalled)
    
    // Verify only 1 email being sent
    XCTAssertEqual(mockEmailService.emailCounter, 1)
    
    // Verify the email's recipient
    XCTAssertEqual(mockEmailService.emailAddress, "manager@email.com")
}

Conclusion

Test doubles are extremely useful when it comes to reducing complexity and separating dependency of a test. Sometimes you can even mix and match each of them to suit your test case requirements. Just remember to always keep your test doubles as thin as possible so that it is easier to maintain.

Here’s the full sample code of this article in Xcode Playground format.


Further Readings


This article should help you get started on using test doubles in your unit tests. If you have any questions, feel free to leave your thoughts in the comment section below. Follow me on Twitter for more article related to iOS development.

Thanks for reading and happy unit testing. 🧑🏻‍💻


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.