WWDC Refactored

Recently, I’ve been discussing ways to architect iOS applications to make them easier to test. Yesterday, I stumbled upon this talk from WWDC ‘17. In this video, the presenter espouses a lot of the same ideas I’ve been advocating here. It’s a great video and I highly recommend it.

There, the presenter refactors a method that might be seen in any application. The original method would be hard or impossible to unit tests due to its dependency on the UIApplication singleton. They refactor it using several techniques, including mocking a dependency, in order to make the logic testable. The result is a much more testable unit of code. Here is the original method:

@IBAction func openTapped(_ sender: Any) {
  let mode: String
  switch segmentedControl.selectedSegmentIndex {
    case 0: mode = "view"
    case 1: mode = "edit"
    default: fatalError("Impossible case")
  }
  let url = URL(string: "myappscheme://open?id=\(document.identifier)&mode=\(mode)")!
  if UIApplication.shared.canOpenURL(url) {
    UIApplication.shared.open(url, options: [:], completionHandler: nil)
  } else {
    handleURLError()
  }
}

And here is the refactored version:

protocol URLOpening {
  func canOpenURL(_ url: URL) -> Bool
  func open(_ url: URL, options: [String: Any], completionHandler: ((Bool) -> Void)?)
}
extension UIApplication: URLOpening {
  // Nothing needed here!
}
class DocumentOpener {
  let urlOpener: URLOpening
  init(urlOpener: URLOpening = UIApplication.shared) {
    self.urlOpener = urlOpener
  }
  func open(_ document: Document, mode: OpenMode) {
    let modeString = mode.rawValue
    let url = URL(string: "myappscheme://open?id=\(document.identifier)&mode=\(modeString)")!
    if urlOpener.canOpenURL(url) {
      urlOpener.open(url, options: [:], completionHandler: nil)
    } else {
      handleURLError()
    }
  }
}

And this is how you would test it:

class MockURLOpener: URLOpening {
  var canOpen = false
  var openedURL: URL?
  func canOpenURL(_ url: URL) -> Bool {
    return canOpen
  }
  func open(_ url: URL,
      options: [String: Any],
      completionHandler: ((Bool) -> Void)?) {
      openedURL = url 
  }
}
func testDocumentOpenerWhenItCanOpen() {
  let urlOpener = MockURLOpener()
  urlOpener.canOpen = true
  let documentOpener = DocumentOpener(urlOpener: urlOpener)
  documentOpener.open(Document(identifier: "TheID"), mode: .edit)
  XCTAssertEqual(urlOpener.openedURL, URL(string: "myappscheme://open?id=TheID&mode=edit"))
}

This is a huge improvement over the original method. It allows us to test all the logic contained in DocumentOpener. It decouples the application singleton from the logic under test. My only objection is that the test is a little convoluted. As someone unfamiliar with the code, I need to examine the mock object to see how it works and I need to open up the DocumentOpener to understand how it interacts with the MockURLOpener. Additionally, the URLOpening protocol makes the production code harder to reason about. Was the protocol added to facilitate testing or did the writer truly intend for consumers of DocumentOpener to implement multiple URLOpening classes? What if we refactored this using some tips I’ve outlined in previous articles?

class DocumentOpener {
  enum Result {
    case openURL(URL), invalidURLError
  }
  let isURLValid: (URL) -> Bool
  init(isURLValid: (URL) -> Bool) {
    self.isURLValid = isURLValid
  }
  func open(_ document: Document, mode: OpenMode) -> DocumentOpener.Result {
    let modeString = mode.rawValue
    let url = URL(string: "myappscheme://open?id=\(document.identifier)&mode=\(modeString)")!
    if isURLValid(url) {
      return .openURL(url)
    } else {
      return .invalidURLError
    }
  }
}

Arguably this is a little nicer; we’ve removed a few lines of code and a type declaration. The real value comes when attempting to test this unit.

func validURL(_ url: URL) -> Bool {
  return true
}
func testDocumentOpenerWhenItCanOpen() {
  let documentOpener = DocumentOpener(isURLValid: validURL)
  let result = documentOpener.open(Document(identifier: "TheID"), mode: .edit)
  XCTAssertEqual(result, .openURL(URL(string: "myappscheme://open?id=TheID&mode=edit")))
}

As you can see, we are injecting a single method instead of a conforming object. Additionally, we are returning a value representing the action we’d like to perform. The advantage is that our test is reduced from 18 lines to 8, there is almost no arrange code, and the return value is easily verified.

This isn’t always the best approach. If DocumentOpener had a lot of interaction with the system via URLOpening, we’d end up injecting a lot of methods into DocumentOpener. At this point it might make sense to inject a URLOpening object instead.

In conclusion, by avoiding mocks and focusing on value types, it is possible to write tests that are shorter and easier to understand down the road.

I'm a freelance iOS developer based in San Francisco. Feel free to contact me.