Article

Adding Unit Tests to your Existing Project

Remove coupling between components to increase test coverage and reliability.

Overview

Unit tests, that you write using XCTest, can increase the speed of your development work, by giving you confidence that changes and additions don’t cause regressions in app functionality. Adding unit tests to existing projects can be difficult, because design choices made without considering testability can couple together distinct classes or subsystems, making it impossible to test them in isolation. Coupling in software design manifests as a class or function that can only be used successfully when connected to other code that works in a specific way. Sometimes, this coupling means your tests will attempt network connections or interact with the filesystem, which will make the tests slow and their results non-deterministic. Removing the coupling makes it possible to introduce unit tests, but requires code changes in places where you don’t already have test coverage, which can be risky.

Improve the test coverage of your project by identifying a component you’d like to test, and writing a test case that covers the behavior you want to assert. Use a risk-focused approach to prioritization that covers logic in features which have received a high number of user bug reports, or where a regression would have the highest impact.

When the code you’re testing is coupled to another part of your project or a framework class, make the smallest possible change to the code that isolates the component without changing its behavior. Improve the ability to use the class in a test context with reduced coupling, and keep the changes small to reduce the risk associated with each change.

The following sections propose changes that remove couplings in situations where coupling between the code under consideration and another component blocks testing. Each solution demonstrates how an XCTest case works with the changed code to assert its behavior.

Replace Concrete Type with Protocol

When your code relies on a specific class whose behavior makes testing difficult, create a protocol that lists the methods and properties used by your code. Examples of such problematic dependencies include those that access external state, including user documents or databases, or those that don’t have deterministic results, including network connections or random value generators.

The following listing shows a class in a Cocoa app that uses NSWorkspace to open a file, which represents an attachment to an email or instant message. The outcome of the openAttachment(file:in:) method depends on whether the user has an application installed that can handle files of the requested type, and whether the application successfully opens the file. All of these variables could introduce test failures, which would slow down development as you investigate “errors” that turn out to be transient problems unrelated to your code.

import Cocoa

enum AttachmentOpeningError : Error {
  case UnableToOpenAttachment
}

class AttachmentOpener {
  func openAttachment(file location: URL, with workspace: NSWorkspace) throws {
    if (!workspace.open(location)) {
      throw AttachmentOpeningError.UnableToOpenAttachment
    }
  }
}

To test code with this coupling, introduce a protocol that describes how your code interacts with the problematic dependency. Use that protocol in your code, so your class depends on the existence of the methods in the protocol, but not their specific implementation. Write an alternative implementation of the protocol that doesn’t perform the stateful or non-deterministic tasks, and use that implementation to write tests with controlled behavior.

In this listing, a protocol that includes the open(_:) method is defined, along with an extension to NSWorkspace that makes it conform to the protocol.

import Cocoa

enum AttachmentOpeningError : Error {
  case UnableToOpenAttachment
}

protocol URLOpener {
  func open(_ file: URL) -> Bool
}

extension NSWorkspace : URLOpener {}

class AttachmentOpener {
  func openAttachment(file location: URL, with workspace: URLOpener) throws {
    if (!workspace.open(location)) {
      throw AttachmentOpeningError.UnableToOpenAttachment
    }
  }
}

In tests, write a different implementation of the URLOpener protocol that doesn’t depend on the apps installed on the user’s computer.

class StubWorkspace : URLOpener {
  func open(_ file: URL) -> Bool {
    return isSuccessful
  }

  var isSuccessful: Bool = true
}

class AttachmentOpenerTests: XCTestCase {
  var workspace: StubWorkspace! = nil
  var attachmentOpener: AttachmentOpener! = nil
  let location = URL(fileURLWithPath: "/tmp/a_file.txt")

  override func setUp() {
    workspace = StubWorkspace()
    attachmentOpener = AttachmentOpener()
  }

  override func tearDown() {
    workspace = nil
    attachmentOpener = nil
  }

  func testWorkspaceCanOpenAttachment() {
    workspace.isSuccessful = true
    XCTAssertNoThrow(try attachmentOpener.openAttachment(file: location, with: workspace))
  }

  func testThrowIfWorkspaceCannotOpenAttachment() {
    workspace.isSuccessful = false
    XCTAssertThrowsError(try attachmentOpener.openAttachment(file: location, with: workspace))
  }
}

Replace Named Type with Metatype Value

When one class in your app creates and uses instances of another class, and the created objects introduce testing difficulties, it can be hard to test the class where they’re created. Parameterize the type of the created object and use a required initializer to create an instance. Examples of this difficult testing situation include a controller that creates a new document on the filesystem in response to a user action, or a method that interprets JSON received from a web service and creates new Core Data managed objects that represent the received data.

In each of these cases, because the objects are created by the code you want to test, you can’t pass in a different object as a parameter to the method. The object doesn’t exist until it’s created by your code, at which point it’s of the type that has the untestable behavior.

The listing below shows a UIDocumentBrowserViewControllerDelegate that creates and opens a document object when the user picks a document in the browser. The document object it creates reads and writes data to the file system, so its behavior is not easy to control in a unit test.

class DocumentBrowserDelegate : NSObject, UIDocumentBrowserViewControllerDelegate {
  func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) {
    guard let sourceURL = documentURLs.first else { return }
    let storyBoard = UIStoryboard(name: "Main", bundle: nil)
    let documentViewController = storyBoard.instantiateViewController(withIdentifier: "DocumentViewController") as! DocumentViewController
    documentViewController.document = Document(fileURL: sourceURL)
    documentViewController.modalPresentationStyle = .fullScreen

    controller.present(documentViewController, animated: true, completion: nil)
  }
}

To remove the coupling between the code you’re trying to test and the objects it creates, define a variable on the class under test that represents the type of object it should construct. Such a variable is called a metatype value. Set the default value to the type the class already uses. You’ll need to ensure that the initializer used to construct instances is marked required. This listing shows the document browser view controller delegate with that variable introduced. The delegate creates documents with the type defined by the metatype value.

class DocumentBrowserDelegate : NSObject, UIDocumentBrowserViewControllerDelegate {
  var DocumentClass = Document.self

  func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) {
    guard let sourceURL = documentURLs.first else { return }
    let storyBoard = UIStoryboard(name: "Main", bundle: nil)
    let documentViewController = storyBoard.instantiateViewController(withIdentifier: "DocumentViewController") as! DocumentViewController
    documentViewController.document = DocumentClass.init(fileURL: sourceURL)
    documentViewController.modalPresentationStyle = .fullScreen

    controller.present(documentViewController, animated: true, completion: nil)
  }
}

Set a different value for the metatype in tests, so your code constructs an object that doesn’t have the same untestable behavior. In tests, create a “test dummy” version of the document class: a class with the same interface, but which doesn’t implement the behavior that makes it hard to test. In this case, a dummy document class should not interact with the file system.

class DummyDocument : Document {
  static var opensSuccessfully:Bool = true
  static var savesSuccessfully:Bool = true
  static var closesSuccessfully:Bool = true

  override func save(to url: URL, for saveOperation: UIDocument.SaveOperation, completionHandler: ((Bool) -> Void)? = nil) {
    // don't save anything, just call the completion handler
    guard let handler = completionHandler else { return }
    handler(StubDocument.savesSuccessfully)
  }

  override func close(completionHandler: ((Bool) -> Void)? = nil) {
    guard let handler = completionHandler else { return }
    handler(StubDocument.closesSuccessfully)
  }

  override func open(completionHandler: ((Bool) -> Void)? = nil) {
    guard let handler = completionHandler else { return }
    handler(StubDocument.opensSuccessfully)
  }
}

Replace the document type with the dummy type in your test case’s setUp() method, so the delegate being tested creates instances of the dummy document type, which behave deterministically in the tests.

class DocumentBrowserDelegateTests: XCTestCase {
  var delegate: DocumentBrowserDelegate! = nil

  override func setUp() {
    delegate = DocumentBrowserDelegate()
    delegate.DocumentClass = StubDocument.self
  }

  override func tearDown() {
  }

  // test methods here
}

Subclass and Override Untestable Methods

When a class combines custom logic with interactions or behavior that make the class hard to test, introduce a subclass that overrides some of the class’s methods to make the others easier to test. It’s common to design classes that contain both app-specific logic, and interactions with the environment or frameworks that render behavior difficult to control in tests. A common example is a UIViewController subclass, which has app-specific code in its action methods and also loads views or presents other view controllers.

Introducing tests for the custom app logic is desirable, to ensure that this logic works as expected and to protect against regressions. The complexity of controlling or working around the interactions between the class and the environment make testing the logic difficult.

As an example, the following iOS view controller populates a label with a user name found in a profile object (and, in principle, it could populate other UI elements with other fields in the profile). It uses UserDefaults to find the path to a file, tries to load that as a dictionary, then uses the values in that dictionary to populate the UI.

struct UserProfile {
    let name: String
}

class ProfileViewController: UIViewController {
    @IBOutlet var nameLabel: UILabel!
    
    override func viewDidLoad() {
        self.loadProfile() { maybeUser in
            if let user = maybeUser {
                self.nameLabel.text = user.name
            } else {
                self.nameLabel.text = "Unknown User"
            }
        }
    }
    
    func loadProfile(_ completion: (UserProfile?) -> ()) {
        let path = UserDefaults.standard.string(forKey: "ProfilePath")
        guard let thePath = path else {
            completion(nil)
            return
        }
        let profileURL = URL(fileURLWithPath: thePath)
        let profileDict = NSDictionary(contentsOf: profileURL)
        guard let profileDictionary = profileDict else {
            completion(nil)
            return
        }
        guard let userName = profileDictionary["Name"] else {
            completion(nil)
            return
        }
        let profile = UserProfile(name: userName as! String)
        completion(profile)
    }
}

To overcome this complexity, subclass your view controller and “stub out” the methods that produce complex, untestable interactions, by overriding them with simpler methods. Use the subclass in your tests to verify the behavior of the custom logic, which you don’t override. You may also need to introduce a metatype value, if the code under test creates an instance of the target type.

The following listing introduces a subclass, StubProfileViewController, which doesn’t have all of the coupling to UserDefaults and the file system in its parent class. Instead, it uses a UserProfile object that’s configured by the caller. Tests using this subclass can easily provide exactly the object needed to trigger the logic they’re testing.

class StubProfileViewController: ProfileViewController {
    var loadedProfile: UserProfile? = nil

    override func loadProfile(_ completion: (UserProfile?) -> ()) {
        completion(loadedProfile)
    }
}

Two tests are needed to fully cover the behavior of viewDidLoad(). One test checks whether the name is correctly set from the profile, if the profile can be loaded. The other test checks whether the placeholder value for the name is used, if the profile is not loaded.

class ProfileViewControllerTests: XCTestCase {
    var profileVC: StubProfileViewController! = nil
    
    override func setUp() {
        profileVC = StubProfileViewController()
        // configure the label, in lieu of loading a storyboard
        profileVC.nameLabel = UILabel(frame: CGRect.zero)
    }

    override func tearDown() {
    }

    func testSuccessfulProfileLoadingSetsNameLabel() {
        profileVC.loadedProfile = UserProfile(name: "User Name")
        profileVC.viewDidLoad()
        XCTAssertEqual(profileVC.nameLabel.text, "User Name")
    }
    
    func testFailedProfileLoadingUsesPlaceholderLabelValue() {
        profileVC.loadedProfile = nil
        profileVC.viewDidLoad()
        XCTAssertEqual(profileVC.nameLabel.text, "Unknown User")
    }
}

Inject a Singleton

If your code uses a singleton object to gain access to globally-available state or behavior, turn the singleton into a parameter that can be replaced to support isolation for testing. Singleton use can be spread throughout a codebase, which makes it hard to know the singleton’s state when it’s used by the component you’re trying to test. Running tests in different orders may produce different outcomes.

Commonly-used singletons, including NSApplication and the default FileManager, have behavior that’s dependent on external state. Components that use these singletons directly introduce more complications for reliable testing.

In this example, a Cocoa view controller represents part of a document inspector in a news app. When the view controller’s represented object changes, it posts a notification to the default notification center, to which other components in the app subscribe.

let InspectedArticleDidChangeNotificationName: String = "InspectedArticleDidChange"

class ArticleInspectorViewController: NSViewController {
  var article: Article! = nil

  override var representedObject: Any? {
    didSet {
      article = representedObject as! Article?
      NotificationCenter.default.post(name: NSNotification.Name(rawValue: InspectedArticleDidChangeNotificationName), object: article, userInfo: ["Article": article!])
    }
  }
}

While a test could register with the default notification center to observe this notification, the use of the singleton notification center makes it possible for other components in the app to interfere with the test outcome. Other code could post the notification, remove observers, or run its own code in response to the notification, all of which may interfere with the outcome of the test.

Replace direct access to the singleton object with a parameter or property that can be controlled from outside the component under test. In the app, continue to use the singleton as the collaborator for the component. In tests, supply an alternative object that’s easier to control.

The following listing shows the result of applying this change to the article inspector view controller listed above. The view controller posts to the notification center defined in its notificationCenter property, which is initialized to be the default center.

let InspectedArticleDidChangeNotificationName: String = "InspectedArticleDidChange"

class ArticleInspectorViewController: NSViewController {
  var article: Article! = nil
  var notificationCenter: NotificationCenter = NotificationCenter.default

  override var representedObject: Any? {
    didSet {
      article = representedObject as! Article?
      notificationCenter.post(name: NSNotification.Name(rawValue: InspectedArticleDidChangeNotificationName), object: self, userInfo: ["Article": article!])
    }
  }
}

In a test case you can substitute a different notification center, which is not used elsewhere in the test suite or the app, and therefore is isolated from the behavior of other tests and modules.

class ArticleInspectorViewControllerTests: XCTestCase {

  var viewController: ArticleInspectorViewController! = nil
  var article: Article! = nil
  var notificationCenter: NotificationCenter! = nil
  var articleFromNotification: Article! = nil
  var observer: NSObjectProtocol? = nil

  override func setUp() {
    notificationCenter = NotificationCenter()
    viewController = ArticleInspectorViewController()
    viewController.notificationCenter = notificationCenter
    article = Article()
    observer = notificationCenter.addObserver(forName: NSNotification.Name(InspectedArticleDidChangeNotificationName), object: viewController, queue: nil) { note in
      let observedArticle = note.userInfo?["Article"]
      self.articleFromNotification = observedArticle as? Article
    }
  }

  override func tearDown() {
    notificationCenter.removeObserver(observer!)
  }

  func testNotificationSentOnChangingInspectedArticle() {
    viewController.representedObject = self.article
    XCTAssertEqual(self.articleFromNotification, self.article, "Notification should have been posted")
  }
}

You may need to combine this change with those described in the article sections: Replace Concrete Type with Protocol, and Subclass and Override Untestable Methods, to create the alternative object you use in the test in place of the singleton. You’ll need to do this where the singleton supplies behavior that’s difficult to control in a test, like FileManager or NSApplication.

See Also

Testing

Testing Your Xcode Project

Detect logic failures, UI problems, and performance regressions with XCTest