Articles, podcasts and news about Swift development, by John Sundell.

Unit Testing

Published on 08 Jan 2019

Using unit tests, and other forms of automated testing, can be a great way to protect a code base against regressions and reduce the need for manual testing. Unit tests can also be a nice tool to use when trying to reproduce tricky bugs, and to find the source of memory leaks.

A unit test is essentially just a function that invokes some of our code, and then asserts that the right thing happens. These functions are implemented within special classes called test cases, which — in the case of Xcode’s default testing framework, XCTest — are subclasses of XCTestCase.

Let’s say that we’ve written a function that applies a discount code to reduce the price of a product, like this:

extension Product {
    mutating func apply(_ coupon: Coupon) {
        let multiplier = 1 - coupon.discount / 100
        price *= multiplier
    }
}

The above kind of model mutation code is both pretty important to get right — since it’s usually a core part of our logic — and something that’s easy to verify in isolation, which makes it an ideal candidate for a unit test.

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.

Let’s create a test case which we’ll use to verify all logic related to our Product model. We’ll call it ProductTests, and add our first test method called testApplyingCouponCode, which we’ll later fill in with our actual testing code:

import XCTest
// In order to get access to our code, without having to make all
// of our types and functions public, we can use the @testable
// keyword to also import all internal symbols from our app target.
@testable import ProductApp

class ProductTests: XCTestCase {
    func testApplyingCoupon() {

    }
}

XCTest’s test runner will automatically look for all methods defined on a test case that start with the word “test”, and will call all those methods when our test suite is run.

Now to actually start testing our logic — within our testApplyingCoupon method, we’ll start by creating the values needed to perform our test — in our case a Product and a Coupon to apply to it. We then apply the coupon, and finally use XCTAssertEqual (which is part of a special suite of assert functions specifically designed for testing) to verify that the product’s new price is the same as we’d expect:

func testApplyingCoupon() {
    // Given
    var product = Product(name: "Book", price: 25)
    let coupon = Coupon(name: "Holiday Sale", discount: 20)

    // When
    product.apply(coupon)

    // Then
    XCTAssertEqual(product.price, 20)       
}

Above we use a structure called Given, When, Then — which is commonly used in order to make tests easier to read and debug, especially when working within a team. It can sort of be read as Given these conditions, when these actions are performed, then this is the expected outcome”.

While creating the values needed within a test’s Given section can be a great solution for simpler tests, when we want to use the same value or object multiple times throughout a test case a better option might be to create it in a single place — using that test case’s setUp method.

XCTest automatically calls setUp before running each test, which makes it an ideal place to reset our test case’s state and create fresh copies of any objects we need to run our tests. Let’s take a look at another example, in which we’re testing a ShoppingCart class by creating an instance of it in setUp:

class ShoppingCartTests: XCTestCase {
    // Normally, it can be argued that force unwrapping (!) should
    // be avoided, but in unit tests it can be a good idea for
    // properties (only!) in order to avoid unnecessary boilerplate.
    private var shoppingCart: ShoppingCart!

    override func setUp() {
        super.setUp()
        // Using this, a new instance of ShoppingCart will be created
        // before each test is run.
        shoppingCart = ShoppingCart()
    }

    func testCalculatingTotalPrice() {
        // Given: Here we assert that our initial state is correct
        XCTAssertEqual(shoppingCart.totalPrice, 0)

        // When
        shoppingCart.add(Product(name: "Book", price: 20))
        shoppingCart.add(Product(name: "Movie", price: 15))

        // Then
        XCTAssertEqual(shoppingCart.totalPrice, 35)
    }
}

Another thing worth noting about the above two examples is that when we’re performing our asserts (using XCTAssertEqual), the values we’re comparing against are hard-coded. While in production code we probably would’ve wanted to move such hard-coded values into properties or variables, in tests it’s usually a good idea to keep verification values as literals — as it lets our tests remain as simple as possible, which usually makes them easier to both read and maintain.

Unit testing can at first seem like a really complex topic that involves learning several advanced techniques before getting started — and while that might be true for more sophisticated tests, adding simple ones (like we did above), can be a great way to start out — and even the simplest tests can provide a ton of value.

Thanks for reading! 🚀