Unit Testing in Swift 101

Mohseen Shaikh
make it heady
Published in
8 min readJul 30, 2020
Photo by Zan on Unsplash

What is unit testing?

In computer programming, unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use (Wikipedia).

In short, this tells us that unit testing is:

  1. Validating the individual unit of our source code (e.g. all the public or internal functions of a class)
  2. Checking for the correct behavior of our units when used with other modules (e.g. are inputs on a view properly persisted in storage module?)

Getting started

The most challenging part of writing unit tests is getting started. Most of the time, we start developing features using some architectural pattern. But when writing test cases, we need to use a suitable architecture or modify the app to work with Test Driven Development(TDD).

Testing the app makes our code more isolated and decoupled (which turns into a benefit when changes are required later on).

There are many architectural patterns we can use to start our unit testing project. What we choose depends upon the requirements, future scope, scalability and maintainability. One of the testable architectures we use is MVVM (Model — View — ViewModel).

MVVM Architecture

Model: Also known as entities, which consist of the domain data

View: User Interface, which displays the data and interaction with the end-user (e.g. button tap, swipe)

ViewModel: The business logic and mediator between View and Model. This is UIKit independent, and both manages the presentation state for Views and updates the Models. In general, it is responsible for altering the Model by reacting to the user’s actions performed on the View, while updating the View with changes from the Model.

Check out more architectural pattern options here.

Dealing with dependencies

Unit tests involve testing of isolated code. This isolation can be achieved by removing the dependencies in the module.

Usually, we need an API Service to fetch data from the server and perform operations or display it to the View. This ‘APIService’ is like a dependency on our ViewModels. We generally need it in the VMs, as that’s where most of the business logic is handled.

Talking about isolation, rather abstraction which is used in Object-Oriented Programming can be obtained using the Protocols in Swift. As an example, the property apiService in the ViewModel will always capture the instance that conforms to APIServiceProtocol. This not only gives us separation of the Network Layer but also helps us mock these dependencies, which is helpful for testing.

Types of dependencies:

  1. Constructor Injection: we always pass the dependency while initializing the module
  2. Property Injection: this happens when the class is already initialized and we inject the dependent module via a property
let loginViewController = UIStoryboard.instantiate(.login)
loginViewController.viewModel = LoginViewModel()

3. Functional Injection: a separate function is used to inject the dependency to the module

let loginViewController = UIStoryboard.instantiate(.login)
loginViewController.setup(viewModel: LoginViewModel())

Test doubles

Test doubles and its types

A test double is any component we eventually use to satisfy object dependencies.

A few examples of test doubles are Dummy, Stub, Spy, Mock and Fake.

Dummy: This is a dependency object which is used just to shut the compiler warning. Think of it as an object that does nothing, but is needed to satisfy the condition.

/// A dependent object for loading images in your app.
protocol ImageLoader {
func downloadImage(url: String)
}
// Real Implementation
class ImageDownloader: ImageLoader {
func downloadImage(url: String) {
AF.downloadImage(url)
}
}
// Dummy implementation which will do nothing.
class DummyImageLoader: ImageLoader {
func downloadImage(url: String) {
/// Nothing to do here
}
}

As a general rule, a dummy should do nothing and return as close to nothing as possible.

Stub: This provides a predefined output for the indirect inputs our SUT will need for tests. Stubs are frequently used in a lot of test cases.

We know we need a Stub when we need to control the indirect inputs of our SUT, and if we don’t want to have any verification logic on it.

// The account manager dependency protocol.
protocol AccountManager {
var isUserAuthenticated: Bool { get }
func login(email: String, password: String,
completionHandler: @escaping (Bool, String?) -> Void)
}
class AccountManagerStub: AccountManager { // We implement the dependency property.
var isUserAuthenticated = true
func login(email: String, password: String,
completionHandler: @escaping (Bool, String?) -> Void) {
completionHandler(true, "Success")
}
}

Spy: This works exactly like a Stub, but also records the information from the function that was invoked.

protocol WishlistService {
func add(productID: String)
}
class CartController { let wishlistService: WishlistService

init(wishlistService: WishlistService) {
self.wishlistService = wishlistService
}

// Order later product method will call a add method which
// returns nothing and, hence it will be dificult to test that add was called.
func orderLater(product: String) {
wishlistService.add(productID: product)
}
}
class WishlistServiceSpy: WishlistService {

// In addition to the interface implementation.
// The spy has some extra properties that
// will remember how the dependency was called.
var wishlistedProductCount: Int = 0

func add(productID: String) {
wishlistedProductCount += 1
}
}

Here, add() method returns nothing and has been called from the CartController’s instance method orderLater().

How could we determine that the add() method is actually being executed while testing? This can be resolved by making a WishlistServiceSpy while actually testing the CartController. Doing so will spy a number of times the add method was called, and give us a test double for testing the CartController.

Mock: This is similar to a Spy in terms of remembering information, but with an added advantage: it knows the exact behavior that should take place. While a Spy just gets the indirect values in the test, a Mock verifies whether the output is as expected.

protocol WishlistService {
func add(productID: String)
}
class CartController { let wishlistService: WishlistService

init(wishlistService: WishlistService) {
self.wishlistService = wishlistService
}

// Order later product method will call a add method which
// returns nothing and, hence it will be dificult to test that add was called.
func orderLater(product: String) {
wishlistService.add(productID: product)
}
}
class WishlistServiceMock: WishlistService {

// In addition to the interface implementation.
// The spy has some extra properties that
// will remember how the dependency was called.
var wishlistedProductCount: Int = 0

// This will verify the output for the test.
func verifyAddWasCalled() -> Bool {
wishlistedProductCount > 0
}
func add(productID: String) {
wishlistedProductCount += 1
}
}

Ideally, we should use Mocks over Spies in most cases. We only use a Spy when using a Mock is not possible or overly complicated.

Fake: This is usually used when SUT is dependent on some other complex framework or components. It will be a simplified version of that framework or any component.

When some of the dependencies cannot be replaced with any static values or other test doubles, a fake should be used. As such, fakes will always comprise with some logic. They can also grow depending upon the external components’ logic and test cases.

class FakePersistenceClass: PersistenceClass {    // A dictionary we will use to store during unit tests
private var fakeStorage: [String: Any] = [:]

override func save(user: String) {
fakeStorage[defaultName] = user
}

// Override the real get method to fake user retrieval behavior
override func getUser() -> String? {
return fakeStorage[defaultName]
}
}

Generally, most of the database or persistence layer abstraction can be used as a fake database while testing, as we don’t actually require SUT to store our data to a real database.

In the above example, the FakePersistenceClass overrides the actual PersistenceClass to fake save and getUser methods by using a dictionary to store and retrieve the values.

Finally… Let's start writing our test cases!

We usually start by testing the business logic of the application, which is the crux of any feature. While not every developer does this, test cases should be written first and then implemented in a strict TDD environment.

In this example, we will use Apple’s default test suite XCTest for writing our test cases. There are also other frameworks, like Quick and Nimble.

We will be testing the business logic in the ViewModel of the application. We have a sample HomeViewModel class, and we will be writing test cases for it.

Pro Tip: Unit tests are run within a unit testing target. If your app does not yet have one, first add a “Unit Testing Bundle” target using Xcode’s File > New > Target... menu before getting started.

First, we need to add a test class in the Unit Tests target. Select the target folder and add a New File( File > New > File…) and select Unit Test Case Class.

Once we hit the Next button, we will have some boilerplate code for the XCTest class that we will have to modify with our requirements. We will be testing the behavior of two things:

  1. Verify that the view model executes the service call and fetches the data.
  2. Confirm that once we get the data from our backend, it tells our View to update itself.

For every test case, there should be a test function starting with the word test which indicates for XCTest that it is a testing function (e.g. func test_viewDidLoad()). However trivial the test case, it is imperative to identify minute mistakes or code flow in our features.

  1. We always use Subject Under Test (SUT) to determine which component we are testing.
  2. setUp() is used to allocate our SUT. In our case, HomeViewModel and tearDown() is used to clean up all the allocations. These functions always run before every test execution, which helps in distinguishing behavior testing.
  3. In our first test to verify if the VM is fetching the data from the server, we have used MockAPIService which mocks the API layer. This mockApiService has a property, didfetchPopularMovies, which determines if it was fetched or not.
  4. In the second test case, we have also used a spy viz homeViewSpy to verify if the View gets updated when the data is fetched from the backend. It also checks if the data source has loaded with data and notifies its View, which can be validated with updateViewCalled in homeViewSpy.

Note: We have used two test doubles. One of them is Mock to spoof the Service layer’s implementation as a local one which isn't calling actual web service but behaving like one. Another is a Spy to remember if the View was updated.

In conclusion…

Using TDD is in itself may seem tedious, but helps produce robust, scalable and maintainable software.

It may be tricky at first to understand the importance of writing trivial test cases. But in the long run, it actually helps with major code changes when test cases start to fail, because we can identify the fixes for code flow beforehand.

Thanks for reading!

Take a peek at the project with all the test cases here.

References

XCTest | Apple Developer Documentation

Effective Unit Testing

Unit Tests (Swift): Mocking the right way.

P.S : We’re hiring at Heady! 👋 If you would like to come work with us on some really cool apps please check out our careers page.

--

--

Mohseen Shaikh
make it heady

Software Development Engineer — II @ heady.io | iOS App Developer | Ex @Reliance Jio | https://github.com/MohseenShaikh