Leonardo Maia Pugliese
Holy Swift

Holy Swift

Unit Testing UIViewController Dismiss closure argument in Swift

Unit Testing UIViewController Dismiss closure argument in Swift

Let's test it all!

Leonardo Maia Pugliese's photo
Leonardo Maia Pugliese
·Dec 23, 2021·

7 min read

Subscribe to my newsletter and never miss my upcoming articles

Hallo alle mensen, Leo Hier.

Today we will attack some tricky unit testing code. First let's talk about testing UIViewController with unit tests.

I'm very akin to test everything. Literally. I think we should have unit tests for everything. Every little piece of code we write should be automated tested. I know that we all have constraints in our day-to-day life. So testing 100% of things is almost impossible. This way we should choose what battle to fight.

This article attacks one thing that can be a little tricky to test. The dismiss behaviour of an UIViewController, and to be fair this is only important if you need to trigger some action after the dismiss or checking some . Because if not, you are just testing the UIKit itself.

No worry about all the details, in the article's end there's a link this project on GitHub. The important thing is study the concepts of dependency injection and unit testing.

Let's code! But first...

The Painting

The painting is a 1598 masterpiece called Saint Catherine of Alexandria from the legendary Painter Michelangelo Merisi da Caravaggio. He was a master Italian painter, father of the Baroque style, who led a tumultuous life that was cut short his by his fighting and brawling.

The history of is fascinating. Saint Catherine of Alexandria was a popular figure in Catholic iconography. She was of noble origins, and dedicated herself as a Christian after having a vision. At the age of 18 she confronted the Roman Emperor Maximus (presumably this refers to Galerius Maximianus). Imprisoned by the emperor, she converted his empress and the leader of his armies. Maximus executed her converts (including the empress) and ordered that Catherine herself be put to death on a spiked wheel. The wheel reportedly shattered the moment Catherine touched it. Maximus then had her beheaded.

I chose this painting because it's the face we developers do when someone asks on the PR if we plan to add unit tests to new code. 👀

The problem

You have want to unit test that a log service is called when dismiss a UIViewController.

Create a new project called UnitTestDismissTutorial and make sure that the Include Tests option are checked. Clean up the Main storyboard, we will use view code.

Let's start updating the SceneDelegate.swift file. Replace all your code for the code below:

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }

        self.window = UIWindow(windowScene: windowScene)
        window?.rootViewController = HomeViewController()
        window?.makeKeyAndVisible()
    }
}

It will not build but don't worry.

Now create the HomeViewController.swift file and copy this code to it:

import UIKit

final class HomeViewController: UIViewController {

    private let logManager: LoggingManager
    private var detailsViewController: DetailsViewController

    init(logManager: LoggingManager = LogManager(), detailsViewControllerFactory: DetailsViewControllerFactory = DefaultDetailsViewControllerFactory()) { // Mark 1
        self.logManager = logManager
        self.detailsViewController = detailsViewControllerFactory.create()
        super.init(nibName: nil, bundle: nil)
        view.backgroundColor = .white
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(openModal), userInfo: nil, repeats: false)
        Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(closeModal), userInfo: nil, repeats: false)
    }

    @objc private func openModal() {
        present(detailsViewController, animated: true)
    }

    @objc private func closeModal() { 
        detailsViewController.dismiss(animated: true, completion: { [weak self] in
            self?.logManager.log(text: "DetailsViewController was dismissed!!!") // This is our unit test goal
        })
    }
}

On Mark 1 you can observe what is the most important when we talk about testing code. To be able to be tested the HomeViewController should delegate all of his dependencies to other objects. This example shows that the HomeViewController needs two things two work properly: one object that can log things, and one object that provides a DetailsViewController.

To make things interesting the present and the dismiss methods are called by separated timers. This implies in an extra layer of complexity on our tests, but do not worry we will get there.

We want to test the what happen inside the dismiss function closure argument. Is it calling every time? Is it dismissing the view that I think it is?

Let's continue.

Create a LogManager.swift file and copy/paste the code below:

import Foundation

protocol LoggingManager {
    func log(text: String)
}

struct LogManager: LoggingManager {
    func log(text: String) {
        print(text)
    }
}

Create a new file called DetailsViewController.swif :

import UIKit

class DetailsViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .red
    }
}

And finally create the file DetailsViewControllerFactory.swift file and copy/paste the code below:

import Foundation

protocol DetailsViewControllerFactory {
    func create() -> DetailsViewController
}

class DefaultDetailsViewControllerFactory: DetailsViewControllerFactory {
    func create() -> DetailsViewController { DetailsViewController() }
}

That is the default implementation of the Factory. We need to do that to be able to inject whatever we want inside the HomeViewController when unit testing.

Unit Testing

Now go to your tests and copy paste the code below:

import XCTest
@testable import UnitTestDismissTutorial

class UnitTestDismissTutorialTests: XCTestCase {

    func test_HomeViewController_DismissDetailsViewController_LogManagerCalled() throws {
        let dismissExpectation = XCTestExpectation(description: "Should Dismiss DetailsViewController") // Mark 1
        let logManager = LogManagerSpy() // Mark 2

        let detailsViewControllerFactoryMock = DetailViewControllerFactoryMock { dismissRunCount in
            XCTAssertEqual(1, logManager.logRunCount)
            XCTAssertEqual(1, dismissRunCount)
            dismissExpectation.fulfill()
        } // Mark 3

        let sut = HomeViewController(logManager: logManager, detailsViewControllerFactory: detailsViewControllerFactoryMock) // Mark 4

        sut.loadViewIfNeeded() // Mark 5

        wait(for: [dismissExpectation], timeout: 10) // Mark 6
    }
}

extension UnitTestDismissTutorialTests {

    final class LogManagerSpy: LoggingManager { // Mark 7
        var logRunCount = 0
        func log(text: String) {
            logRunCount += 1
        }
    }

    final class DetailViewControllerFactoryMock: DetailsViewControllerFactory { // Mark 8
        let detailsViewController = DetailsViewControllerMock()

        init(expectation: @escaping (Int)->()) {
            detailsViewController.expectation = expectation
        }

        func create() -> DetailsViewController {
            return detailsViewController
        }
    }

    final class DetailsViewControllerMock: DetailsViewController { // Mark 9
        var dismissRunCount = 0
        var isAnimated = false
        var expectation: ((Int)->())?

        override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
            dismissRunCount += 1
            isAnimated = flag
            completion?()
            expectation?(dismissRunCount)
        }
    }
}

Let's explain all marks:

  1. The dismissExpectation is only necessary because we have to wait until the completion of the dismiss action by the DetailsViewController.
  2. In mark 2 we create our LogManagerSpy, it is a spy because it's partially implemented. In our case we just want to make sure it's called only once by the dismiss.
  3. Now when we create the DetailViewControllerFactoryMock object we need to pass a closure ( behaviour ), we made like this so we could test when all the functions were called and we could fulfil the dismissExpectation.
  4. Now we inject the the factory mock and the manager spy to the HomeViewController. Note that in the production code the initialisation of HomeViewController is just with no arguments because we are using the default initialisers in it.
  5. This is a very important call because it triggers all the UIViewController lifecycle, ending up calling the viewDidLoad function.
  6. Here is how much time we want to wait for the fulfilment of the test. This value can be high because we know the test will finish in less than 0.005 seconds, but if you can't control the time, it's time to refactor your code so you can have more control over it.
  7. Mark 7 is where we create the LogManagerSpy, it conforms to the LoggingManager protocol so we can inject it in the HomeViewController, and of course we add the logRunCount so we can check in the tests how many times we are calling it on our tests.
  8. Mark 8 is the build of the Factory mock. Notice that now we are not returning the DetailsViewController but a mock of it?
  9. And finally we create a child class of the DetailsViewController. The secret about this implementation is that we can override the dismiss function of it and intercept all his behaviours.

Now you just have to press command + U to run the unit test and we finished the exercise today.

Summary

Today we observed a lot of unit test techniques. We created a UIViewController that everything was injected using protocols so we can mock in our tests and how crucial was that to make it testable. Also we override the dismiss function of a child implementation to be able to check it in tests. And finally testing asynchronous code with expectations and fulfilments.

You can check the full code in my GitHub repository.

That's all my people, I hope you liked as I enjoyed write this article. If you want to support this blog you can Buy Me a Coffee or just leave a comment saying hello. You can also sponsor posts and I'm open to writing freelancing! Just reach me in LinkedIn or Twitter for details.

Thanks for the reading and... That's all folks.

credit: image

Did you find this article valuable?

Support Leonardo Maia Pugliese by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
 
Share this