Unit Testing: Dealing with unreliable objects that are members of a targeted class

Today I came across the following problem: I was doing Test-Driven Development for a simple iOS app. I separated the UICollectionViewDataSource methods into a class itself to make testing easier and to avoid ending up with a god ViewController. So, I now have a class called MyAppDataSource that is an outlet of my ViewController:

class ViewController: UIViewController {
    @IBOutlet var collectionView: UICollectionView!
    @IBOutlet var customDataSource: (UICollectionViewDataSource & UICollectionViewDelegate)!

    override func viewDidLoad() {
        collectionView.delegate = customDataSource
        collectionView.dataSource = customDataSource
    }

    .
    .
}

And the MyAppDataSource class:

class MyAppDataSource: NSObject, UICollectionViewDataSource {
    var databaseManager: DatabaseManager!
    
    override init() {
        databaseManager = DatabaseManager.instance
        super.init()
        populateDatabaseWithDefaultValues()
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return databaseManager.getNumberOfValues()
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CustomCell
        cell.name.text = "Some Text"
        return cell
    }

I test-drove the above class, however, there is a problem. The DatabaseManager is a wrapper class for managing CoreData objects. Hence, unit testing this class which depends on the databaseManager is not ideal as the databaseManager relies on an external entity (the SQLite file). I instead created a MockDatabaseManager subclass of DatabaseManager that overrides some methods to redirect the data storage from the SQLite file to memory, making this a mock object. More information about this implementation in this post.

Now, I need a way to mock the MyAppDataSource class. I thought to create a subclass and override the databaseManager with an instance of the MockDatabaseManager class. It turns out that it’s not possible to override stored properties in Swift (at least in Swift 5.0).

I ended up doing this slightly different, but equally clean:

MyAppDataSource.swift:

class MyAppDataSource: NSObject, UICollectionViewDataSource {
    var databaseManager: DatabaseManager {
        return DatabaseManager.instance
    }

    override init() {
        super.init()
        populateDatabaseWithDefaultValues()
    }

    .
    .
    .

Simply for the MyAppDataSource class, the stored property databaseManager now becomes a computed property, allowing space for a derived class to override it.

MockMyAppDataSource.swift:

class MockMyAppDataSource: MyAppDataSource {
    private var _databaseManager: DatabaseManager!
    
    override var databaseManager: DatabaseManager {
        if _databaseManager == nil {
            _databaseManager = MockDatabaseManager()
        }
        
        return _databaseManager
    }
}

Simply, the mock version of the class, overrides the databaseManager to instantiate a mock version of the databaseManager.

This is a good way to implement this as we ensure that if we instantiate a MockMyAppDataSource object, the testing of this class can cover all the functionality of its superclass, MyAppDataSource, but with a mock databaseManager object. This way we can still extend the MyAppDataSource class with more functionality and test it against a MockMyAppDataSource instance.

Unit Testing

This way, when I want to unit test the MyAppDataSource, I instantiate a MockMyAppDataSource object like so:

class MyAppDataSourceTests: XCTestCase {
    var sut: MyAppDataSource! // System Under Test
    var collectionView: UICollectionView!
    
    override func setUp() {
        super.setUp()
        
        // Here
        sut = MockMyAppDataSource()
        collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout.init())
        collectionView.dataSource = sut
    }

    // Test cases for sut (System Under Test)
    .
    .
    .
}
© 2019 Rafael Papallas
Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.