Unit Test in Swift: A Starting Guide

Learn key concepts about testing and how you can start adding Unit Tests to your iOS project

Bruno Lorenzo
6 min readFeb 6, 2024
Image by Author

Do you like fixing bugs?

I'm sure you don't.

Having a proper unit test structure will help you avoid headaches in the future.

Besides helping to make sure your code still works as expected after every change you make, unit tests have several implicit benefits, such as:

  1. Being more confident when shipping new code to production.
  2. Increasing overall code quality.
  3. Reducing code-base complexity.

Let’s see the key concepts behind tests and how to add them to your iOS project.

What are Unit Tests?

Just to be on the same page, we can say that unit tests are pieces of code that check if a small, isolated, and well-defined block of our application’s code (aka unit) is working as expected.

Test Structure

There are several components we need to understand before we jump in into the coding part.

Image by Author
  • Test methods are the methods that validate portions of our code. Here is where we create and develop the test. These methods will produce a test result, either pass or fail.
  • Test classes are a set of test methods. Usually, they group a specific logic to test, for example: Authentication.
  • Test bundles contain a group of test classes and represent one of two test types: Unit or UI.
  • Test plans are a group of test bundles, so we can include both Unit and UI tests here. In the test plans, we set up a list of configurations to be considered when running tests.

I will be using the code of my Coffee Shop Demo, you can check the source code here

We start by creating a Test Bundle

Go to File > New > Target and search for Unit Testing Bundle. Give the target a proper name, typically your app name ending with Tests.

By creating a test bundle, a test plan will be automatically created for us with default configurations.

Image by Author

Setting up our Unit Tests

Every programming language has a framework or library to execute unit tests. In the iOS development world, we have XCTest framework.

Now that we have our test bundle, we can add a new test class. Create it from the navigation panel or the menu (File > File > Unit Test Case Class).

Notice that our test class is a subclass of XCTestCase. This class provides us with all the operations needed to write and execute unit tests.

We want to test the logic behind the OrderManager class, so a good name for our test class could be OrderTests.

// Add a new order 
func add(_ order: Order) throws { ... }
// Repeat the last order, adding it to the list of orders
func repeatLastOrder() { ... }

Let’s create a test method for each function inside our test class.

To access our app’s code from our test target without going file by file and sharing them, we need to add @testable instruction and import our app.

import XCTest
@testable import Coffee_Shop_App

final class OrderTests: XCTestCase {

override func setUpWithError() throws {}
override func tearDownWithError() throws {}

func testAddNewOrder() {}
func testRepeatLastOrder() {}
}
Image by Author

Before each test starts, XCTest executes setUpWithError() function. So here we need to set the initial state for our tests. This could be creating instances of specific classes, injecting dependencies, configuring some variable values, and so on.

On the other side, after each test ends, tearDownWithError() is called. This is a good place to clean up anything that we consider.

Method test names should start with the word test to be executed.

Now we have everything in place to start creating our Unit Tests

First, we need to access OrdersManager.

  1. Add a property to the class.
  2. Instance it in setUpWithErro() method.
  3. Cleanup the state in tearDownWithError() method.

We’re using the Singleton pattern in the demo just for simplicity.

@testable import Coffee_Shop_App

final class OrderTests: XCTestCase {
private var ordersManager: OrdersManager!

override func setUpWithError() throws {
ordersManager = OrdersManager.shared
}

override func tearDownWithError() throws {
ordersManager.removeAllOrders()
ordersManager = nil
}

func testAddNewOrder() {}
func testRepeatLastOrder() {}
}

If you’re getting started with unit tests, using the pattern Arrange / Act / Assert would be a good initial point. The idea is to break down the tests into those steps:

  1. Arrange → Create all the necessary objects or data that the test will use.
  2. Act → Execute the method or function you want to test.
  3. Assert → Compare the results that you get with the expected ones.
final class OrderTests: XCTestCase {
private var ordersManager: OrdersManager!

override func setUpWithError() throws {
ordersManager = OrdersManager.shared
}

override func tearDownWithError() throws {
ordersManager.removeAllOrders()
ordersManager = nil
}

func testAddNewOrder() {
// 1 - Arrange
let orderItems: [OrderItem] = [
.init(item: AnyMenuItem(Coffee.flatwhite), size: .regular, quantity: 1),
.init(item: AnyMenuItem(Food.chickenSandwich), size: .regular, quantity: 1),
]
let order = Order(items: orderItems)

// 2 - Act
try? ordersManager.add(order)

// 3 - Assert
XCTAssertEqual(ordersManager.orders.count, 1)
}

func testRepeatLastOrder() {
// 1 - Arrange
let orderItems: [OrderItem] = [
.init(item: AnyMenuItem(Coffee.flatwhite), size: .regular, quantity: 1),
.init(item: AnyMenuItem(Food.chickenSandwich), size: .regular, quantity: 1),
]

// 2 - Act
let order = Order(items: orderItems)
try? ordersManager.add(order))
ordersManager.repeatLastOrder()

// 3 - Assert
XCTAssertEqual(ordersManager.orders.count, 2)
}
}

Running Unit Tests directly from Xcode

We can run all tests included in a configuration set, all tests included in a test bundle, all tests included in a test class, a group of specific test methods, or just one test method at a time.

To run all tests, click on Product > Test. From the Test navigator, we can run all tests, a specific group, or just one.

Once we’ve run them, we will get a test report with the results. In addition, we can see our test results directly from the Test navigator and inside the test classes.

Image by Author

Test coverage

After we run our tests, we can enable test coverage information inside the code that has been tested.

Image by Author

In green, we see the portion of code that was reached in the last test run, and in red we see the portion of code that wasn’t. In the demo, we’re missing the throwing part, which is a consequence of checking the minimum amount for ordering.

If we notice this scenario, we could add a new unit test to check this and run the tests again.

Using different XCTAssert

There are a bunch of functions we can use when asserting test results. Here are a few examples.

You can check Apple’s documentation for the full list.

// 1 - Check that a throw function doesn't throw
XCTAssertNoThrow(try ordersManager.add(order))

// 2 - Check that a throw function, throws an error
XCTAssertThrowsError(try ordersManager.add(order)) { error in
if let appError = error as? AppError, let errorType = appError.type as? OrderError {
XCTAssertEqual(errorType.code, 2)
} else {
XCTFail("Wrong error type triggered")
}
}

// 3 - Check that an optional value is not nil
let lastCoffeeDescription = ordersManager.getLastCoffee()
XCTAssertNotNil(lastCoffeeDescription)

Add a custom fail message to every assertion.

XCTAssertEqual(ordersManager.orders.count, 1, "Order was not added correctly")

Things to keep in mind

  • It is always better to spend time writing tests than fixing bugs.
  • Don’t see unit tests as an extra workload. Try to add them as part of your development workflow.
  • Have in mind the FIRST principle. Every unit test should be Fast, Independent, Repeatable, Self-validating, and Timely.
  • When a test fails, be careful, you’ll be tempted to adapt your tests. And while these may be right in some cases (if the test was written wrong), usually is the logic that you’re testing that is wrong.
  • Don’t aim for 100% test coverage. You could have 100% coverage, and yet have tests for 1 of 10 possible scenarios. Instead, focus on writing quality tests.
  • Use tests as an opportunity to find small code refactors that will increase your overall code quality.

Have any questions? Feel free to drop me a message! 🙂

  • 🤓 Join me on X for regular content on iOS development tips and insights
  • 🚀 Check out my GitHub where I share all my example projects

--

--

Bruno Lorenzo

Software Engineer | Innovation Manager at www.houlak.com | Former iOS Tech Lead | I write about iOS, tech, and producitivy