Unit Testing iOS in Swift — Part 2: A Testable Architecture

The hardest part about unit testing is starting. One of the reasons for this is that not all architectures can be unit tested, at least not easily. If you want to write unit tests (and in Part 1 I explained why you should), you need to pay attention to the architecture of your app, and how to set it up.

I’ll go more into detail about that in a bit, but I would like to stress one key point first:

In the process of making our architecture more testable, we’ve made it better on all other axis. Testable design is very often good software design.

In transitioning to a more testable architecture, we’ve made the components of our apps more isolated from each-other, especially from external frameworks. This made it much easier to deal with change, which is the only constant in software development. We also started paying much more attention to how we build our apps at the start, saving us time in the long run.

Let’s talk about exactly how we did this.

Dealing with ViewControllers

Mocking ViewControllers is hard. Testing ViewControllers is hard. So we just don’t do it.

Wait, what?

We use our own version of the MVVM architecture. I won’t go into too much detail as there are lots of articles about MVVM, but basically take all of the logic that’s not related to UIKit outside of your ViewController. These are all model transformations, calling different services, state transformation, error handling, etc. Those all go into the ViewModel. All the ViewController does is bind data from the ViewModel, and sends it user interaction, as well as presentation code, like animations and preparing views. Here’s more articles about MVVM if you’re interested:

There are different schools of thought on MVVM. Some people think the ViewModel should be a value type initialised with the model, we think differently. The ViewModel, in our architecture, is a class that has UI state, talks to different services and provides raw presentable data to the ViewController.

This way, we have a really thin and dumb UIKit-related layer, and almost all of the logic in our app is easily testable.

(Note: You can definitely test ViewControllers. But frankly, MVVM is a better architecture paradigm than MVC in my opinion.)

Dependency injection

Since each “unit” in a unit test is completely isolated, we need a way to isolate each component in our app. This means that we need a way to provide “sterile” dependencies to each class we are testing, to know for certain tests won’t fail because of the dependencies.

Dependency injection is is just a fancy phrase for externally providing dependencies to a class. No class ever creates its own dependencies. If it talks to another class, it should be initialised with an instance of that class as a parameter in the initialiser. (this is called constructor injection)

Another key aspect of this is that **every dependency is declared as an **instance of a protocol. That way, we can easily give the class/struct whatever object we want in the initialiser, as long as that object has those couple of methods the class needs.

class LoginViewModel: LoginViewModelProtocol {
  
  private let fbService: FacebookServiceProtocol
  private let userService: UserServiceProtocol
    
  init(fbService: FacebookServiceProtocol, 
       userService: UserServiceProtocol) {
    self.fbService = fbService
    self.userService = userService
  }

This allows us to provide our own controlled dependencies to the class, making sure it’s completely isolated from the rest of the app. Also, this allows us to do neat little things with the mock dependencies, but more on that in a bit.

This is also a good idea even if you’re not writing tests, as it decouples one class from another. Changing the implementation of one class won’t break another, since the other class is not dependent on a concrete implementation, but rather just the existence of protocol members.

External Frameworks

What if a class has a dependency to an external framework? How do you deal with a class depending on NSURLRequest or Core Data? Use the same basic principle, with a little twist.

We make wrappers around external Frameworks. We call these helpers, and basically they import whatever external framework they wrap around, propagate function calls to them, and transform the results into a type that we can use in our codebase without depending on the external framework. As a general rule of thumb, the helper is the only one that ever imports a framework.

Logic inside the helper should be as thin as possible, because much like the ViewController, they’re pretty hard to test. However, make sure you’re not just re-implementing the same functionality of the framework, try to make helpers conform to the coding standards and needs that you have.

protocol NetworkHelperProtocol {

  func jsonRequest(
    path path: String, 
    params: [String: AnyObject]?, 
    completion: ServiceResult<AnyObject> -> Void)
 
  func dataRequest(
    path path: String, 
    completion: ServiceResult<NSData> -> Void)
 
  
  func postRequest(
    path path: String, 
    parameters: [String: AnyObject]?, 
    completion: ServiceResult<AnyObject> -> Void)
 
  func deleteRequest(
    path path: String, 
    parameters: [String: AnyObject]?, 
    completion: ServiceResult<AnyObject> -> Void)
}

import Alamofire

struct NetworkHelper: NetworkHelperProtocol {
  //method implementations...
}

The same principle as with other dependencies applies, all classes receive instances of the protocol the helpers conform to, not a concrete implementation.

This leads to a huge benefit: If, for instance, you decide Core Data is just too complex and switch to Realm, all you need to change is one class.

So if a class doesn’t provide its own dependencies, who does? The Service Factory.

Service Factory

The ServiceFactory is basically a collection of get-only computed variables that create dependencies for the whole application. For instance, a ViewModel that has a dependency of type APIServiceProtocol, will be constructed as ViewModel(apiService: ServiceFactory.apiService).

struct ServiceFactory {
  
  static var dataService: DataService {  
    return DataService(
      networkHelper: NetworkHelper(baseURL: AppConfig.baseUrl)
    )
  }
  
  static var facebookService: FacebookService {
    return FacebookService(fbHelper: FacebookHelper())
  }
}

This means we have only one place in our app where we decide which class talks to another. I think of this as like a control room, any developer can look at this class and know what’s going on in the app at a glance.

It also provides a single place for swapping our implementations of dependencies, so for instance, converting a class from a singleton to a regular instance takes about 5 seconds. One thing we do often is split a class into two if it’s getting too large, this lets us do that in a really short amount of time.

It’s also great for UI tests and debugging, because depending on the launch arguments you can provide stub and mock dependencies to your classes, so your UI tests are isolated from the network, or a feature on the backend is still being worked on.

Okay, so we have an app where we can swap out a real implementation with mocks, but how do we do this?

Mocks

Mocks are classes or structs that conform to the same protocol as a dependency of the class you are testing, and are provided to the class in unit tests. Mocks usually have no-op methods or some small functionality useful for unit testing.

As a general rule, mocks go in the test target. This helps us to not pollute our application target with a bunch of mock implementations.

You don’t only mock dependencies though. In order to test completely, you’ll often have to mock delegate objects. The same exact principle applies to these, but keep in mind that you need to verify delegate methods are being called.

Swift, because of its limited runtime access, doesn’t yet have a nice mocking framework like Mockito for Android or OCMock for Objective-C, so we write mocks by hand.

This can be a tedious process, but it also gives you more freedom than mocking frameworks. I find most mocks follow the same general design:

  1. Implement the protocol you are mocking
  2. Each method has a corresponding methodDidGetCalled boolean property on the mock, and optional requestedParameterX properties. The implementation of those methods usually boils down to setting those properties. You can later check if the method got called with the correct parameters by the tested class.
  3. If a method has some sort of return value or a completion handler, there’s a methodXShouldFail boolean property on the struct. The implementation of the method would then be to check that boolean and return either a successful or an unsuccessful result. This is useful for checking if the tested class handles failures.
    struct UserServiceMock: UserServiceProtocol {
      
      var shouldFailLogin = false
      var didLogin = false
      var requestedEmail: String? 
      var requestedPassword: String?
      
      func loginUser(email email: String, password: String, 
         completion: ServiceResult<User> -> Void) {
         
        didLogin = true
        requestedEmail = email
        requestedPassword = password
            
            
        if shouldFailLogin {
          completion(.Failure(.CannotParse))
        } else {
          completion(.Success(MockUser()))
        }
      }
    }

Writing tests

If you’ve set up your app correctly, and know what to test, this part is actually fairly easy. It’s just verifying the behaviour of the test.

There are lots of libraries that can help you out with testing. First of all, Apple’s XCTest integrates with Xcode and is fairly robust, albeit a bit wordy to write. There are also third party frameworks like Quick and Nimble. We use Nimble with XCTest.

For simple pure functions: supply data, check the result. Repeat.

func testValidatesPasswordCorrectly() {
  expect(FieldValidator.validate(.Password(""))).to(beFalse())
  expect(Validator.validate(.Password("asdfg"))).to(beFalse())
  expect(Validator.validate(.Password("asdfgh"))).to(beFalse())
  expect(Validator.validate(.Password("asdf1"))).to(beFalse())
  expect(Validator.validate(.Password("asdfg1"))).to(beTrue())
}

For methods that call other methods, make sure to check if the method got called, and whether or not it passed the right parameters.

func testLoginCallsUserService() {

  viewModel.login(email: "asdf@mail.com", password: "1234asd")

  expect(userService.didLogin).to(beTrue())
  expect(userService.requestedPassword).to(equal("1234asd"))
  expect(userService.requestedEmail).to(equal("asdf@mail.com"))
}

Similarly, make sure your class notifies delegates or calls the correct callbacks.

func testCompletesOnAlreadyLoggedIn() {
  var didComplete = false
  fbService.userIsLoggedIn = true
  viewModel.onComplete = { didComplete = true }
  expect(didComplete).to(beTrue())
}

Now it’s all about doing this a couple of more times until you test everything that you can!