Unit Testing in Swift

Yağız Erbay
adessoTurkey
Published in
9 min readJan 16, 2023

How to Get Started with iOS Unit Testing: Fundamentals

What is unit testing?

In computer programming, a “unit test” is a software testing method that initializes a small chunk of our project and verifies its behavior independently from other parts. Most common unit tests contain three phases:

First, it initializes a small piece of code it wants to test (also known as the “system under test”, or SUT) after that, it activates the system under test (usually by calling a method); and finally, it observes the resulting behavior. If the observed behavior is consistent with the expectations, the unit test passes; otherwise, it fails, indicating that there is a problem somewhere in the system under test.

These three unit test phases are also known as Arrange, Act, and Assert, or simply AAA.

Advantages of unit tests

  • Unit tests reduce or eliminate potential bugs in new and existing features.
  • Tests reduce development costs when a code change is required.
  • You can improve the design without breaking it.
  • You can refactor your code easily (at least you realize where your refactored code fails before it becomes a bigger concern).
  • Unit tests force you to plan before you code.
  • And most importantly, unit tests act as a fail-safe mechanism against most of our programmers’ worst nightmare: making a change in a piece of code and not knowing what is going to break, crash, or duplicate etc., you name it.

Disadvantages of unit tests

  • They increase the amount of code that needs to be written.
  • Unit tests cannot and will not catch all bugs in an application. There is no way it can test every logic path or find integration and system defects.
  • It will increase the development time so that your development needs may not fall in line with the project owners’ tight schedules (deadlines).

How to add a unit test target to the project?

If you are starting your project from scratch, you should add a unit test target to your Xcode project.

There are two different approaches to doing that:

  • First one is during project creation, by selecting Include Unit Tests checkbox then clicking Next.

If you already have a project, then;

  • First go to File > New > Target.
  • Select Unit Testing Bundle.
  • Then click Next

Apple’s XCTestCase

Okay, people, if you are at this stage, then your project is ready to roll and Xcode has already created a unit test class for you. This class inherits the XCTestCase class, which will act as a base class for all unit tests that you will be adding in the future. A standard test case contains setup and teardown functions. These functions are called before and after each of the test functions, respectively. This cycle allows us to set up prerequisites before the tests are run and reset any variables, references, etc. after the completion of each unit test.

To know more about the setUp() and tearDown() cycles, you can check Apple’s official documentation here.

Starting a function in a XCTestCase class with “test” tells Xcode that the indicated function is a unit test. For each unit test function, you will see an empty diamond shape aligned with the test method declaration listed in the gutter. Clicking on the given diamond icon will run the specific unit test.

Let’s code!

Let’s start with a basic project to compare vehicle types:

import Foundation

class Vehicle {
private var miles = 0
private var type: VehicleType

init(type: VehicleType) {
self.type = type
}

func startEngine(minutes: Int) {
var speed = 0

switch type {
case .PassengerAircraft:
speed = 575
case .FighterJet:
speed = 1320
case .HighSpeedTrain:
speed = 217
case .Car:
speed = 100
case .Bicycle:
speed = 10
}

self.miles = speed * (minutes / 60)
}

func returnMiles() -> Int {
return self.miles
}
}

enum VehicleType {
case PassengerAircraft
case FighterJet
case HighSpeedTrain
case Car
case Bicycle
}

Now, we can initialize and use the vehicle object “mercedes” in a ViewController:

import UIKit

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
let mercedes = Vehicle(type: .Car)
mercedes.startEngine(minutes: 60)
print(mercedes.returnKilometers())
}

}

I admit that it is not my dream project, though it will be enough to understand the basic concept of unit testing.

Creating a test case

In order to create a new unit test in iOS:

  • From a navigator panel, click UnitTestSampleTests.
  • Then click New File…

After that ,you will be taken to another screen, as shown below.

  • Select Unit Test Case Class.
  • In choose options, type class name: VehicleTests and press Next.

When you click Next, you will be taken to another screen. Here you will select where the new VehicleTests file should be created. Make sure you create it in your Unit Tests Group, and also make sure it is only a part of the unit test target, not the app target.

With that, we created another test class, although they are actually the same classes that are inheriting from XCTestCase. We will be using VehicleTests to develop our test cases for the vehicle class. You may delete UnitTestSampleTests class as well because we won’t be using it anymore.

import XCTest

class VehicleTests: XCTestCase {

override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}

override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
}

func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}

}

Let’s go over vehicle tests class

Use @testable to import your application

Since our Vehicle file is not included in our newly created test target, we need to import our main target with @testable. That ensures all code with internal access are available to your test class.

import XCTest
@testable import UnitTestSample

Declare the properties you want to test

We will declare them as implicitly unwrapped optionals (!) because by the time our test case has prepared our properties, we verify that they will be initialized and ready to test one another.

class VehicleTests: XCTestCase {

var mercedes: Vehicle!
var boeing: Vehicle!

}

Override the setUpWithError() method

This is a setup method. It lets us initialize variables as needed before each test method is called. Vehicle class will be injected with its corresponding types.

override func setUpWithError() throws {
mercedes = Vehicle(type: .Car)
boeing = Vehicle(type: .PassengerAircraft)
}

Override the tearDownWithError() method

We also want to make sure that we clear everything out when each test finishes running. It lets us clean up variables, set them to nil, etc., after each test method is called.

override func tearDownWithError() throws {
mercedes = nil
boeing = nil
}

Write a test method

If everything is ready, then let’s create our first test method. In this example, we will be comparing our vehicles against each other. In order to demonstrate this comparison we will write a test method that simply verifies a commercial airplane is faster than a car.

func testPlaneFasterThanCar() {
//Act
let minutes = 60

//Arrenge
mercedes.startEngine(minutes: minutes)
boeing.startEngine(minutes: minutes)

//Assert
XCTAssertTrue(boeing.returnKilometers() > mercedes.returnKilometers())
}

Now let’s run the test. Click on the diamond on the left of the function declaration of testPlaneFasterThanCar(). That should run the test. If startEngine(minutes: Int) method which is declared in vehicle class is working as intended, our test result should come back as passed with a green checkmark to signify that the intended test has passed.

Async method usage

Thus far, we have learned how to develop unit tests and how to access methods written in our main project module. Now, let’s learn how to write a unit test for an async method (ex. network call).

Add the following code example to the end of VehicleTests:

func testFetchPostList() {
let exp = expectation(description:"fetching post list from server")
let session: URLSession = URLSession(configuration: .default)
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")
guard let customUrl = url else { return }

session.dataTask(with: customUrl) { data, response, error in
XCTAssertNotNil(data)
exp.fulfill()
}.resume()
waitForExpectations(timeout: 5.0) { (error) in
print(error?.localizedDescription ?? "error")
}
}

Indicated method above verifies that “fetching post list from server” from server returns desired output or else. To control the async method, we used XCTestExpectation as an object. The purpose of using this object is to initialize the expectation first and wait for the intended time until the expectation is fullfilled.

Alright then, let’s go over the function logic:

  1. In the first step, we will create an expectation(description:) that returns an XCTestExpectation object, which of course is stored in exp. The description parameter acts as a descriptor for what you expect to see.
  2. In the second step, we will call waitForExpectations(timeout:handler:). This method will keep the test running until all created expectations are fulfilled or timeout interval ends (interval is going to execute itself after the indicated number that has been set, e.g. 5.0).
  3. In the third and the final step, we will call exp.fullfil() if the given result meets our expectations.

You can also check these links for further information;

The final version of the vehicle test class should look like this:

import XCTest
@testable import UnitTestSample

class VehicleTests: XCTestCase {

var mercedes: Vehicle!
var boeing: Vehicle!

override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
mercedes = Vehicle(type: .Car)
boeing = Vehicle(type: .PassengerAircraft)
}

override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
mercedes = nil
boeing = nil
}

func testPlaneFasterThanCar() {
//Act
let minutes = 60

//Arrenge
mercedes.startEngine(minutes: minutes)
boeing.startEngine(minutes: minutes)

//Assert
XCTAssertTrue(boeing.returnKilometers() > mercedes.returnKilometers())
}

func testFetchPostList() {
let exp = expectation(description:"fetching post list from server")
let session: URLSession = URLSession(configuration: .default)
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")
guard let customUrl = url else { return }

session.dataTask(with: customUrl) { data, response, error in
XCTAssertNotNil(data)
exp.fulfill()
}.resume()
waitForExpectations(timeout: 5.0) { (error) in
print(error?.localizedDescription ?? "error")
}
}
}

Conclusion

Making a routine of writing unit tests helps us immensely when writing modular pieces of code and assures us that they pass extreme cases as well. Unit tests reduce the possible bugs that may come up in the future while the main codebase is expanding.

Thank you for reading, and happy coding!

--

--