Unit Testing in iOS Development

Fadi Sayfi
5 min readMay 20, 2023

Unit testing is an essential part of software development, ensuring each part of your code operates as anticipated. As we venture into the realm of iOS development, unit testing remains a cornerstone of efficient, reliable software creation. Since the introduction of SwiftUI, Apple’s innovative, declarative UI framework, the process of writing unit tests has become even more intuitive and critical. In this article, we aim to explore the essentials of unit testing in iOS, focusing particularly on SwiftUI applications.

Photo by Christina @ wocintechchat.com on Unsplash

What is Unit Testing?

Unit testing involves testing individual components or ‘units’ of a software. The purpose is to validate that each unit of the software performs as designed. In software terms, a unit could be a method, function, procedure, etc. For iOS development, this could mean testing a single function or method within your View, ViewModel, or Model.

XCTest Framework

XCTest is a robust testing framework provided by Apple, enabling developers to write and perform unit tests for iOS applications. It offers a comprehensive set of APIs to define tests, run them, and measure their execution performance.

To add a test case, you create a new Swift file that inherits from XCTestCase, then generate methods within this class that start with the word "test". XCTest will automatically identify these as unit test methods.

SwiftUI Unit Testing Example: Counter App

Consider a simple SwiftUI counter app that has a label displaying the count and two buttons to increase and decrease the count.

Here’s how you might structure your ViewModel:

import SwiftUI

class ViewModel: ObservableObject {
@Published var count: Int = 0

func increaseCount() {
count += 1
}

func decreaseCount() {
count -= 1
}
}

We would then use this ViewModel in our ContentView:

import SwiftUI

struct ContentView: View {
@ObservedObject var viewModel = ViewModel()

var body: some View {
VStack {
Text("\(viewModel.count)")
.font(.largeTitle)

HStack {
Button {
viewModel.decreaseCount()
} label: {
Text("-")
.padding(.horizontal)
}
.tint(.pink)

Button {
viewModel.increaseCount()
} label: {
Text("+")
.padding(.horizontal)

}
.tint(.green)
}
.buttonStyle(.borderedProminent)
.font(.title2.bold())
}
.padding()
}
}

Our Counter application is now functional. Next, we need to write our unit tests. We’ll generate tests to verify our increment and decrement functions are performing correctly.

Create a new target of type “Unit Testing Bundle”

Make sure the target is selected

You should now have a new target and a new file called SimpleCounterTests.swift you can delete testPerformanceExample() and testExample() for now, and import your app to have access to its classes and views:

import XCTest
@testable import SimpleCounter

final class SimpleCounterTests: XCTestCase {

override func setUpWithError() throws {

}

override func tearDownWithError() throws {

}

}

Declare new property inside the class called viewModel and initialize it inside setUpWithError() . In tearDownWithError() assign nil to viewModel to reset its value. Your file should look something similar to:

import XCTest
@testable import SimpleCounter

final class SimpleCounterTests: XCTestCase {
// Implicitly unwrapped (!)
// because by the time our test case has set it up,
// we can guarantee it will be initialized inside setUpWithError()
var viewModel: ViewModel!

override func setUpWithError() throws {
try super.setUpWithError()
viewModel = ViewModel()
}

override func tearDownWithError() throws {
viewModel = nil
try super.tearDownWithError()
}
}

Next the fun part!, we need to write our unit tests. Add two new functions with names starting with a test (so Xcode can recognize them as Unit Tests), the first function will increase the count by 1 and verifies that the new count equals to 1, the second one will increase the count by 3 times, and decrease 1 time, can you guess what the value of “count” now?

Here is how the entire file should look like by now:

import XCTest
@testable import SimpleCounter

final class SimpleCounterTests: XCTestCase {

// Implicitly unwrapped (!)
// because by the time our test case has set it up,
// we can guarantee it will be initialized inside setUpWithError()
var viewModel: ViewModel!

override func setUpWithError() throws {
try super.setUpWithError()
viewModel = ViewModel()
}

override func tearDownWithError() throws {
viewModel = nil
try super.tearDownWithError()
}

func testIncreaseCount() {
viewModel.increaseCount()
XCTAssertEqual(viewModel.count, 1, "Count was not incremented correctly")
}

func testDecreaseCount() {
for _ in 0..<3 {
viewModel.increaseCount()
}

viewModel.decreaseCount()

XCTAssertEqual(viewModel.count, 2, "Count was not decremented correctly")
}
}

To run these tests, simply press command + U in Xcode, or select “Test” from the “Product” menu. All tests should pass (hopefully) and you should see green checkmarks next to the class name and next to each test function.

Pro Tip: You can click on any green checkmark to run individual test cases.

In our example, we used XCTAssertEqual to verify “count” after increasing and decreasing its value, XCTest framework has many more assertions to explore, here is a list of the most commonly used:

  1. XCTAssert(condition, message): This is the most basic assertion. It verifies that the condition is true. If not, it fails and logs an error message.
  2. XCTAssertEqual(a, b, message): This assertion verifies that a equals b. If a does not equal b, it fails and logs an error message.
  3. XCTAssertNotEqual(a, b, message): This assertion verifies that a does not equal b. If a equals b, it fails and logs an error message.
  4. XCTAssertNil(a, message): This assertion verifies that a is nil. If a is not nil, it fails and logs an error message.
  5. XCTAssertNotNil(a, message): This assertion verifies that a is not nil. If a is nil, it fails and logs an error message.
  6. XCTAssertTrue(condition, message): This assertion verifies that the condition is true. If the condition is false, it fails and logs an error message.
  7. XCTAssertFalse(condition, message): This assertion verifies that condition is false. If condition is true, it fails and logs an error message.
  8. XCTFail(message): This assertion always fails and logs an error message.

In all of these, the message parameter is optional and used to provide additional context if the test fails.

Remember that while these XCTest assertions are powerful, they’re not a substitute for careful design and thoughtful error handling in your code. They’re tools to help catch and identify issues, not to solve them.

Happy Testing!

--

--