Posted on November 11, 2020

This article will take about 12 minutes to read.

This is my first post that is part of a series about Singletons and Tests in Swift. In this one, I’ll show how to make parts of your system that use a Singleton being testable.

Singleton is a very popular design pattern on Apple’s ecosystem. You probably have faced Network.sharedUserDefaults.standard, or other similar implementations around your project. To be honest, I think you probably have a Singleton made by your team in your project and you are here looking for some solution that helps you add testability to it. The good news is: you found it.

Before we start talking about the solution, let’s go back to the unit testing theory: to write acceptable unit tests you should have control and visibility of inputsoutputs, and states of what you are testing. Let me give a basic example:

Imagine that we have a scene that displays how much discount a customer will get on checkout, based on the price of the product:

struct Product {
    let price: Double
}

class CheckoutInteractor {
    func calculate(with product: Product) -> Double {
        let price = product.price
        if price <= 100 {
            return price * 0.05
        } else {
            return price * 0.10
        }
    }
}

So, easy to test CheckoutInteractor, right? We can inject the product, call calculate(with product: Product) function and check if the result is expected:

private let sut = CheckoutInteractor()

func test_calculate_withProductThatPriceIsHigherThan100_shouldReturn10PercentageDiscount() {
    let product = Product(price: 150)
    
    let discount = sut.calculate(with: product)
    
    XCTAssertEqual(discount, 15)
}

Perfect, for this example we have control of all inputs (Product) and visibility of outputs (the Double that is returned by calculate function). There’s no state here for while.

But now, imagine that we should give an extra discount if our customer is subscribed to a kind of loyalty program and this information can change any time while the customer is using the app. For our luck (actually just for the purposes of this article), this information is stored in a Singleton.

enum LoyaltyStatus {
    case subscribed
    case unsubscribed
}

class CustomerManager {

    static var shared: CustomerManager = .init()

    private init() {}
    
    private var customerLoyaltyStatus: LoyaltyStatus?
    
    func setCustomerLoyaltyStatus(_ status: LoyaltyStatus) {
        customerLoyaltyStatus = status
    }

    func getLoyaltyStatus() -> LoyaltyStatus {
        return customerLoyaltyStatus ?? .unsubscribed
    }
}