Basics of Testing in Xcode

Dan Smith
Kin + Carta Created
8 min readJan 27, 2023

Learn the fundamentals of testing and how to write Unit Tests for iOS Projects.

In this article we will learn a little about testing in general, testing applications in Xcode and getting started with the XCTest framework.

Main Sections:

Why Test Our Code?

Well-written tests are essential for maintaining a stable and reliable system. When implemented well they can provide a fast feedback system. When something goes wrong, they help us to quickly identify and fix errors. This in turn helps to both reduce costs and improve the delivery of value in software development.

By shortening feedback loops, we can more efficiently detect and resolve issues. Robust tests also provide us with the foundation to confidently refactor existing code or add new functionality to the system without inadvertently breaking existing behaviour.

Typical software feedback loops (shorter is better):

  • Longest: Crash reports and customer complaints
    - Medium: QA manual testing
    - Rapid: Unit testing

The Testing Pyramid

The testing pyramid is a concept that gives a good starting point to reason about how to balance the testing strategy in our systems.

Image from “Demystifying the Software Testing Pyramid” a LeadDev article.

Categories of Tests

  • Manual Tests are run by a human tester who manually tests the app on a physical device. Testing the user interface, functionality and performance of the software. They are the slowest tests to run but are important particularly in acceptance testing for the client and highlighting issues that automated tests may be unable to detect.
  • UI Tests are automated tests that focuses on testing the user interface of an app. This includes testing the layout, functionality and visual appearance of the app.
  • Integration Tests are automated tests that focus on testing how different parts of the software system, modules, components work together.
  • Unit Tests — are automated tests that focus on testing individual units of code, such as functions, classes or components. They are used to ensure the code behaves as expected, and since they are usually very fast to run, they constitute the base of the pyramid.

FIRST Approach for Good Tests

A handy acronym to keep in mind when writing tests is FIRST short for Fast, Isolated, Repeatable, Self-Verifying, Timely

Fast — tests should build and run quickly. Fast tests mean we get feedback quickly and allow us to run them frequently. Ideally they will build and run in seconds not minutes.

Isolated — each test should be isolated from one another and outside systems. For example, in testing, we would prefer an in-memory database over the live application database store. Side-effects from the test will not persist to impact other tests or pollute live systems.

Repeatable — tests should always produce the same result. There should be no race conditions, nor external factors or services that could make tests fail randomly when run.

Self-Verifying — tests should be able to check their own results and indicate whether they pass or fail. Using the `XCTest` framework we can use the assertion (covered below) to verify results.

Timely— tests should be written and run at the appropriate time. Some teams prefer to write tests after development, while others write them prior to production code (e.g. Test Driven Development). In either case our production code should be well tested before release. Also, additional tests should be added on the discovery of any bugs. This helps to validate that the bug is really fixed and ensure it doesn’t return in the future.

Writing Tests in Xcode

In Xcode we have access to the following two Frameworks geared towards testing:

XCUITest — enables us to write tests that launch the app and automate on screen interactions to validate certain behaviours and interactions with the rest of the app. These are significantly slower to run, and without care can be quite flakey. We will look at creating UITests in a later article.

XCTest — framework to write our unit tests. It enables us to thoroughly test our code and validate it against various inputs and interactions. The rest of this article will focus on Unit Testing with XCTest.

Writing Unit Tests

Naming a test

A common approach to test naming is to separate key information about the test with an underscore in the following order test, functionToTest, inputs, expectedOutput:

// example test naming structure
func test_functionToTest_withInputs_shouldReturnExpectedOutput() {}

// example using above structure
func test_sayHello_withDan_shouldReturnHelloDan() {
// test
}

Arranging your test

Writing isolated tests requires us to implement what is known as the Four-Phase test:

Set up the thing, call the thing, check the thing, destroy the thing — xUnit Test Patterns

Since the compiler usually takes care of clean up this can usually be abbreviated to either of the following phases depending on preference of naming and the intended reader of the test code:

  • Arrange, Act, Assert — (also known as AAA)
  • Given, When, Then — (also known as Gherkin)

Be Assertive — Writing Assertions in Xcode

Below are common types of assertions we use in XCTest with examples.

  • Asserting Equality

XCTAssertEqual used when we want to assert equality between a result and an expected result.

// Hello.swift
class Hello {

func sayHello(to name: String) -> String {
return "Hello, \\(name)!"
}

}

// HelloTests.swift
import XCTest

final class HelloTests: XCTestCase {

func test_sayHello_withDan_shouldReturnHelloDan() {
// Given / Arrange
let sut = Hello()

// When / Act
let greeting = sut.sayHello(to: "Dan")

// Then / Assert
XCTAssertEqual(greeting, "Hello, Dan")
}

}

In typical code intended only for developers to read we may omit the comments for // Given, // When, // Then but keep the single line break between each part of the test function to separate each part of the test.

The above test would look like this:

import XCTest

final class HelloTests: XCTestCase {

func test_sayHello_withDan_shouldReturnHelloDan() {
let sut = Hello()

let greeting = sut.sayHello(to: "Dan")

XCTAssertEqual(greeting, "Hello, Dan")
}

}

Asserting Optionals

  • XCTAssertNil / XCTAssertNotNil — used to assert that an optional value is as expected.
// Hello.swift
class Hello {

func sayHello(to name: String? = nil) -> String? {
guard let name = name else { return nil }
return "Hello, \\(name)"
}
}

// HelloTests.swift
import XCTest

final class HelloTests: XCTestCase {

func test_sayHello_whenGivenAValue_returnsNonNilValue() {
// Arrange
let sut = Hello()

// Act
result = sut.sayHello(to: "Dan")

// Assert
XCTAssertNotNil(result)
}

func test_sayHello_whenGivenNoValue_returnsNil() {
// Arrange
let sut = Hello()

// Act
let result = sut.sayHello()

// Assert
XCTAssertNil(result)
}

}

Asserting Boolean values

To assert Boolean values we can simply use XCTAssertFalse or XCTAssertTrue

// DeathStar.swift
class DeathStar {
var isCompleted: Bool = false

func finishConstruction() {
isCompleted = true
}

}

// DeathStarTests.swift
import XCTest

final class DeathStarTests: XCTestCase {

func test_init_isCompletedIsFalse() {
// Arrange
let sut = DeathStar()

// Act - no action in this case as we are confirming initial state

// Assert
XCTAssertFalse(sut.isCompleted)
}

func test_finishBuilding_setsIsCompletedToTrue() {
// Arrange
let sut = DeathStar()

// Act
sut.finishConstruction()

// Assert
XCTAssertTrue(sut.isComplete)
}

}

Adding Descriptions to our Assertions

When we need to assert more than a single value in a test it can be useful to add a description to the assertion. This will help us when it comes to reading logs and to help us to narrow down the cause of the test failure.

XCTAssertNotNil(sut.aProperty, "- isCompleted")
XCTAssertFalse(sut.someOtherValue, "- someOtherValue")

Now should the test fail we would get a more readable log which can be helpful, particularly when running on CI.

Testing Side Effects With Test Doubles

The focus of unit tests is to test one element of software at a time. However, to check it is working properly the element being tested will often need to interact with other collaborator elements, and confirm that the correct messages were sent or side effects observed.

This is where the the technique of using TestDoubles becomes helpful.

A test double is a generic term for any case where you replace a production object for testing purposes.

Types of Test Doubles

Dummy — objects are passed around but never actually used. Usually they are just used to fill parameter lists.

Fake — object that actually have working implementations, but usually take some shortcut which makes them not suitable for production (e.g. an in memory database is a good example.)

Stub — provide canned answers to calls made during the test, usually not responding to anything outside what’s programmed in for the test.

Spy — stubs that additionally capture information based on how the object is called. (e.g. an HTTPClientSpy that captures urls and responses.)

Mock — is an object preprogrammed with expectations which form a specification of the calls they are expected to receive. They can be setup to throw exceptions if they receive an unexpected call. They can verify they received the correct calls.

Example Use of a Spy Test Double

In the following example we will add a `LaserCrew` collaborator to the DeathStar we tested above. In order to test this effectively we will use a Spy to intercept messages sent within the DeathStar to the laser crew and pass them on.

Adding a collaborator to our DeathStar to fire at a planet. In this case the DeathStar is allocated a LaserCrew on initialisation (constructor injection)

// LaserCrew.swift
class LaserCrew {
func fireAt(_ planet: String) {
// Fire the laser ...
}
}

// DeathStar.swift
class Deathstar {
var isCompleted = false
let laserCrew: LaserCrew

public init(laserCrew: LaserCrew) {
self.laserCrew = laserCrew
}

func finishConstruction() {
isCompleted = true
}

func destroy(_ planet: String) {
guard isCompleted else { return }
laserCrew.fireAt(planet)
}
}

Next in the test code we create a spy subclass to intercept and capture messages. (A common place for this to live would be in a TestHelpers folder).

// LaserCrewSpy.swift 
class LaserCrewSpy: LaserCrew {
var capturedTargets = [String]()

override func fireAt(_ planet: String) {
capturedTargets.append(planet)
super.fireAt(planet)
}
}

We can then use this in our tests to intercept requests to the LaserCrew types and see that the DeathStar messages are being sent as expected.

import XCTest

final class DeathStarTests: XCTestCase {

func test_destroyPlanet_whenConstructionNotComplete_sendsNoMessage() {
let laserCrewSpy = LaserCrewSpy()
let sut = DeathStar(laserCrew: laserCrewSpy)

sut.destroy("Aldoraan")

XCTAssertTrue(laserCrewSpy.capturedTargets.isEmpty)
}

func test_destroyPlanet_whenConstrutionIsComplete_sendsMessage() {
let laserCrewSpy = LaserCrewSpy()
let sut = DeathStar(laserCrew: laserCrewSpy()
sut.finishConstruction()
XCTAssertTrue(laserCrewSpy.capturedTargets.isEmpty) // to assert initial state

// Act
sut.destroy("Aldoraan")

// Assert
XCTAssertEqual(laserCrewSpy.capturedTargets, ["Aldoraan"])
}

}

Tip: Using an array for capturedTargets allows us to check both the order and count of messages sent to the LaserCrewSpy.

An additional benefit of using test doubles is that it encourages us to decouple components from one another by using interfaces. Making our code more flexible and extendible in the future.

Since test doubles are only used by tests they will typically live in either a TestHelper group or if only used within a single test file may be marked `fileprivate` and added to the bottom of the file since it is not reused anywhere.

Summary

In this article we covered the basics of why we write tests, how to write simple Unit Tests in Xcode and the purpose and use of test Doubles.

In the near future, I’ll write some articles on Snapshot and UITesting in Xcode. See you soon!

References and Further Resources

Further Resources

Test Doubles — Martin Fowler — Blog post on test doubles.

iOS Unit Testing by Example: XCTest Tips and Techniques Using Swift — Book on leveraging XCTest to test all aspects of your application.

Demystifying the Software Testing Pyramid — LeadDev article on how to approach testing and the pyramid (source of the pyramid diagram above).

Learn more about testing in general at the amazing iOS Lead Essentials course where you will learn all about developing iOS applications using TDD and how to use TestDoubles to help lead to good decoupled applications.

--

--