stock of timber logs with scratches

Unit Testing basics: what exactly should you test?

If you’re new to Swift development and wondering how to get started writing tests, or you’ve been coding in Swift for some time but aren’t quite sure what parts of your code need to be tested, this post is for you.

As you’re already reading an article about testing, we can assume you’re aware of (or intrigued by) the benefits of writing programmatic tests. But just in case anybody is on the fence, here’s a quick overview of why writing tests results in a more robust and stable code base.

Manual testing

A project without tests must be tested manually. This means running the app on the simulator or on a device, navigating to the required place in the app, performing some action and then visually confirming that the action taken resulted in the correct state. This is incredibly time consuming and in many cases visual confirmation is simply not possible. Imagine you’re about to submit an app to the App Store and you need to go through the app and manually test every single feature to be sure something isn’t broken. Yikes. 😱

Unit testing

It’s far easier to write tests in code as you build each feature of your app to confirm that everything behaves the way you expect. Unit testing allows you to programatically test the state of individual ‘units’ of your project, taking some component in your app responsible for doing some kind of work and asserting that given some input, it will yield a consistent and predictable output that you expect. You no longer have to launch the app and manually fiddle with every feature to gain some semblance hope that everything is ok. Simply run your test suite and bask in the glory of all those lovely green ticks. Aaaaaand relax. ☺️

So what code should be tested?

Let’s look at a very simple example to decide what codes needs to be tested, and what doesn’t. The spec for this example is as follows:

  • Display a list of strings in a table.
  • Tapping a table cell should display an alert to the user.
  • Does the cell contains a string that can be converted into an integer?
  • Yes – show an alert that tells the user if the integer is odd or even.
  • No – simply show the string from the list.
class ViewController: UITableViewController {

    private var viewModel: ViewModel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        assert(viewModel != nil)
    }
    
    func configure(_ viewModel: ViewModel) {
        self.viewModel = viewModel
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        viewModel.numberOfRows
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let item = viewModel.item(at: indexPath.row)
        let cell = UITableViewCell()
        cell.textLabel?.text = item
        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let alert = viewModel.alert(for: indexPath.row)
        showAlert(title: alert.title, message: alert.message)
    }
    
    private func showAlert(title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
}

In this example, we separate the logic into a view model, leaving the causes (i.e. user taps) and effects (i.e. show an alert) in the view controller. We won’t go any further into this particular aspect today, but suffice to say that how you structure your code can make testing either very easy, or near impossible. Writing good tests will likely mean you end up with much better structure in your app code (particularly if you follow TDD and write your tests first).

Many architectures (like MVVM used above) separate the logic away from the causes and effects so that it can be easily tested. We use unit tests to test the logic of our app, so the focus of our tests will be on the view model.

struct ViewModel {
    
    private let items: [String]
    private let alertTitle: String
    private let oddIndexMessage: String
    private let evenIndexMessage: String
    
    init(items: [String],
         alertTitle: String,
         oddIndexMessage: String,
         evenIndexMessage: String) {
        
        self.items = items
        self.alertTitle = alertTitle
        self.oddIndexMessage = oddIndexMessage
        self.evenIndexMessage = evenIndexMessage
    }
    
    var numberOfRows: Int {
        return items.count
    }
    
    func item(at index: Int) -> String {
        items[index]
    }
    
    func alert(for index: Int) -> (title: String, message: String) {
        return createAlertMessage(for: item(at: index), at: index)
    }
    
    private func createAlertMessage(for string: String, at index: Int) -> (title: String, message: String) {
        if let intValue = Int(string) {
            let message = intValue % 2 == 0 ? evenIndexMessage : oddIndexMessage
            return (title: alertTitle, message: message)
        } else {
            return (title: alertTitle, message: string)
        }
    }
}

So we know we need to test this view model, but what parts need to be tested? We need to test the public interface exposed by this view model (that is used by our view controller). This gives us one computed property and two methods that need testing:

  • The number of rows: var numberOfRows: Int
  • The string for a given index: <strong>func</strong> item(at index: Int) -> String
  • The alert title and message for a given index: <strong>func</strong> alert(for index: Int) -> (title: String, message: String)

Let’s create a unit test for this view model.

class ViewModelTests: XCTestCase {

    // ('sut' stands for system under test)
    private var sut: ViewModel!
    
    // a fresh `sut` is created before each test case runs
    override func setUp() {
        sut = ViewModel (
            items: ["1", "2", "3", "4", "Hello!"],
            alertTitle: "Wahoo",
            oddIndexMessage: "Odd",
            evenIndexMessage: "Even")
    }
}

Let’s first test that numberOfRows does indeed return the same number of items passed into the view model, which is 5.

func testNumberOfRows() {
    XCTAssertEqual(sut.numberOfRows, 5) // ✅
}

Now let’s test that item(at:) gives back the correct string. In this particular example, we know upfront what every item in the list should be, so we can test every index.

func testItemAtIndex() {
    XCTAssertEqual(sut.item(at: 0), "1") // ✅
    XCTAssertEqual(sut.item(at: 1), "2") // ✅
    XCTAssertEqual(sut.item(at: 2), "3") // ✅
    XCTAssertEqual(sut.item(at: 3), "4") // ✅
    XCTAssertEqual(sut.item(at: 4), "Hello!") // ✅
}

Easy right? Here is where things get a little more interesting. We need to test the alert message and title for a given index, and this has three possible branches of logic: an odd integer, an even integer, and a string. So we need to write three different tests to exercise each branch of logic on this one method.

func testAlertForItemIsOddInt() {
    let alert = sut.alert(for: 0) // item is "1"
    XCTAssertEqual(alert.title, "Wahoo") // ✅
    XCTAssertEqual(alert.message, "Odd") // ✅
}

func testAlertForItemIsEvenInt() {
    let alert = sut.alert(for: 3) // item is "4"
    XCTAssertEqual(alert.title, "Wahoo") // ✅
    XCTAssertEqual(alert.message, "Even") // ✅
}

func testAlertForItemIsNilInt() {
    let alert = sut.alert(for: 4) // item is "Hello!"
    XCTAssertEqual(alert.title, "Wahoo") // ✅
    XCTAssertEqual(alert.message, "Hello!") // ✅
}

Easy again! Although this is a trivial example, hopefully you’re getting a feel for how this could be applied to more complex logic. Note that there is one method in the view model marked as private that we are not testing.

func alert(for index: Int) -> (title: String, message: String) {
    return createAlertMessage(for: item(at: index), at: index)
}

private func createAlertMessage(for string: String, at index: Int) -> (title: String, message: String) {
    if let intValue = Int(string) {
        let message = intValue % 2 == 0 ? evenIndexMessage : oddIndexMessage
        return (title: alertTitle, message: message)
    } else {
        return (title: alertTitle, message: string)
    }
}

You will often have functions that call other functions in your logic, where you have broken down something large and complicated into smaller pieces, or where some function gets reused multiple times. This is great as it helps you reason about your logic more easily during development.

So should we test this private method? Because this method is marked as private, it’s not even available to us in the test suite…and this is a good thing. Unit tests are only concerned with the public interface of the unit, and not the specific implementation details that drive them.

By testing the public method alert(for index: Int) which makes use of <strong>func</strong> createAlertMessage(for string: String, at index: Int), we have also exercised the private function. You don’t need to test private methods.

I hope this has provided a useful introduction as to what you should be testing in your unit tests, and that you can use this to build up a solid practice of testing your code in future work. It’s not as difficult as it may seem, and can actually be quite fun! You can download the example project from GitHub. I’m really passionate about this subject, so stay tuned for more articles on testing soon! ✅

Stay in the Loop

Subscribe to tapdev and never miss a post.

Total
0
Shares
Previous Post
food healthy leaf pineapple

Check an enum case without a switch statement

Next Post
aerial shot of green milling tractor

A better way to structure Combine with UIKit in MVVM