Hallo allemaal, Leo hier. Today we will talk about unit test your new async functions.

Async/await is a programming pattern that allows developers to write asynchronous code in a synchronous-like style. This pattern makes it easier to write and read asynchronous code and has become popular in many programming languages, including Swift. When it comes to unit testing asynchronous code, the async/await pattern can make things much easier.

With async/await, developers can write unit tests for asynchronous code, in the same way, they would write tests for synchronous code, using the familiar XCTest framework provided by Apple.

Especially talking about SwiftUI, async/ really helps us to create a more fluent code to read and reason about. I really like unit tests. I’ve covered an interesting case on how to test notification centers with various testing techniques.

We also covered unit test UIKit components, for example, here is one article about unit testing an UIButton and all its actions. It is amazing what we can do when we explore the APIs that Apple provides us, and today we will do that with the new structured concurrency framework.

We will explore ways to test async functions and how the new async/await framework can help you with that.

Let’s code! But first…  

 

Painting of the Day

The painting I chose is called The Artist in his Studio and is a 1628 painting by Rembrandt that is currently held by the Museum of Fine Arts in Boston.

The painting depicts an artist’s studio in a realistic style and has been the subject of critical analysis, with one commentator noting the discrepancy between the size of the painting and the size of the canvas depicted within it. The critic described the painting as “rendering something far grander than itself–a painting several times its own size.”

I chose that because that is the face I usually do when I have to add unit testing after writing a bunch of code. It is always better to write unit tests along with the coding.

 

The Problem

You want to use the new async/await functions and you want to cover them with unit testing.

This is commonplace right now. Apple releases a new technology and we iOS developers go running to use it as fast as we can but then the reality comes at the door calling and we should adequate the new technologies to our way of developing. One of the first things that we think about is unit testing.

Today we will check three main cases for with async/await unit testing. We will test a view model that fetches users from a service. I’ll not prepare the view model to receive a service behind an interface, because we already did that in other articles for example in this one where I explain unit testing the notification center. The first one will be the simplest, how to test async functions. The second will be how to use async/await to make old closures-based APIs easier to test. And third, we will refactor a piece of code that internally used task.

The first step will be to set up the code.  

 

Code Setup to Async/Await Unit Testing

Create a new iOS app project with SwiftUI as UI Interface.

Then, create a new Swift file called UserViewModel and copy and paste the code below:

import SwiftUI

struct UserResponse: Decodable { // API decodable
    let data: [User]
}

struct User: Decodable, Identifiable { // API decodable
    var id: String { first_name }
    
    let first_name: String
}

final class UserViewModel: ObservableObject {
    
    @Published var users: [User] = []
    
    func fetchUsers() async throws -> [User] {
        
        let (data,_) = try await URLSession.shared.data(from: URL(string: "https://reqres.in/api/users")!)
        
        guard let users = try? JSONDecoder().decode(UserResponse.self, from: data) else { return [] }
        
        return users.data
    }
    
    func fetchUsersClosure(completion: @escaping ([User]) -> Void) {

        URLSession.shared.dataTask(with: URLRequest(url: URL(string: "https://reqres.in/api/users")!)) { data, _, _ in         
            guard let data,
                  let usersData = try? JSONDecoder().decode(UserResponse.self, from: data) else {
                return completion([])
            }
            
            completion(usersData.data)
        }.resume()
    }
    
    func fetchUsersTask() {

        Task {
            if let users = try? await fetchUsers() {
                DispatchQueue.main.async {
                    self.users = users
                    print("------------------ fetchUsersTask ----------------------")
                    print(users)
                }
            }
        }
    }

}

What’s happening in the code above? We have three functions that fetch the same users from this really good open remote API – reqres. And if you pay attention all the functions are fetching the users in a different way.

  • The first function is fetching users using pure async APIs. This is the best scenario for testing, this is really really simple to test and we will check that later.
  • The second function, fetchUsersClosure, is a vanilla closure-based function. I wanted to add it here because it is really good to show one of the most interesting async techniques. You can use the withCheckedContinuation API to not necessarily transform your old API into a new one, but just to test them.
  • The third function is the most common case in SwiftUI code bases. Is a function that returns Void, aka nothing, and it does an async work that the only to check for results would be waiting for the binding and that would affect our test performances. I don’t want to add a delay on each test to wait for the response and check the published values, that doesn’t make sense.

 

  And to just test them modify the SwiftUI body var of the ContentView file to:

import SwiftUI

struct ContentView: View {
    
    @StateObject var userViewModel = UserViewModel()
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
            
            List {
                ForEach(userViewModel.users) { user in
                    Text("\(user.first_name)")
                }
            }
            
        }.task {
            
            if let users = try? await userViewModel.fetchUsers() {      
                print(users)
            }
            
            userViewModel.fetchUsersTask()

            userViewModel.fetchUsersClosure { users in
                print("--------- from closure ------")
                print(users)
            }
        }
        .padding()
    }
}

The result should be:

swiftui unit testing async await tutorial first result

  Now let’s start the juicy part, how can we unit test each one of the functions in the UserViewModel?  

 

Writing Unit Tests for SwiftUI View Model

As I said before the goal here is not to show the perfect architecture with DI, and that’s why our ViewModel is not receiving and protocol with just the interfaces of the services it needs. Yes, that is something you need to do to test be able to realistically unit test your view models but that is not the goal of this article. We are just looking at techniques on how to test with async/await.

Let’s study the first function.  

 

How to Write Unit Tests for Async Functions

To write an async/await unit test in Swift, you first need to mark the test method with the async keyword, and the XCTest framework will automatically run the test method on a background thread.

Inside the test function, you can use the await keyword to wait for an asynchronous operation to complete, and then write your assertions as you would in a synchronous test. 

Now we will test the first function, the async fetchUsers(). 

Check the code below:

func testFetchUsersAsync() async throws { // just add 'async throws' keywords to the function and you can test any async throws function     
    let users = try await sut.fetchUsers()
        
    XCTAssertTrue(users.count > 0)
}

Simple as this. You can just add async and throws to the function in your tests, and you are able to test async code.

Really cool, isn’t it?  

 

How to Use async/await to Simplify My Closure-Based APIs tests

Let’s test the fetchUsersClosure function.

In the past, that would require us to use expectations and I was never a big fan of expectations, to be honest. I think is a clunky API because the execution of the code goes back and forth, and I find that confusing.

First, we need to introduce at least three more lines to each test that runs with an expectation. We need to create an expectation, then we need to call the fulfillment of that expectation, and then we need to wait for that expectation.

Check the code below using expectations:  

func testAsyncClosureWithNewAsyncAPI() async {
    let expectation = XCTestExpectation(description: "Closure test Based") // expectation boiler plate  
    
    sut.fetchUsersClosure { users in
        XCTAssertTrue(users.count > 0)
        
        expectation.fulfill() // expectation boiler plate
    }
    
    wait(for: [expectation], timeout: 2) // expectation boiler plate
}

And there was one of the only ways to test async code in iOS. Now we have the new structure concurrency, that brings us the power of continuations.

We can rewrite that test like this:

func testAsyncClosureWithNewAsyncAPI() async {
    
    let users = await withCheckedContinuation { continuation in
        sut.fetchUsersClosure { users in
            continuation.resume(returning: users)
        }
    }
    
    XCTAssertTrue(users.count > 0)
}

Really neat, right? We just had to add the async keyword to the function and instead of using expectations all around, we just need to wrap our function inside a withCheckedContinuation block and test against it!

The code is easier to reason and it feels more natural to write an asynchronous test like this.

One last thing I want to discuss is that the best option would be changing the whole fetchUsersClosure to the new async/await APIs. But sometimes the code is too complex and it doesn’t worth touching. This is just a way that you can test your old closure-based API with the async/await capabilities.  

 

Using Task API to Asynchronous Unit Test in Swift

Nowadays is pretty common that a view model has a published var and when we finish fetching the data we just update the published var and SwiftUI takes care of everything. Although this is very practical in the way that we don’t need to worry about integrations and everything works magically, I always like to have control over the calls I’m doing. So in this case we will change the a little bit the fetchUsersTask. 

I discovered recently that the Task API can have return types. Wait a minute! Now we can assert that.

Check the final changes on the code below, first, add this new function to the UserViewModel.swift:

func fetchUsersTaskTestable() -> Task<Array<User>, Error> {
    return Task {
        guard let users = try? await fetchUsers() else { return [] } // you should throw an error here
        return users
    }
}

Now the API is returning the task with the results to be handled by others. And this is how you use it now in SwiftUI:

import SwiftUI

struct ContentView: View {
    
    @StateObject var userViewModel = UserViewModel()
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
            
            List {
                ForEach(userViewModel.users) { user in
                    Text("\(user.first_name)")
                }
            }
            
        }.task {
            let task = userViewModel.fetchUsersTaskTestable()

            if let users = try? await task.value {
                userViewModel.users = users
            }
        }
        .padding()
    }
}

Now that we are returning a Task, we can do the same thing we are doing in SwiftUI but in our test code to assert the result. Check the test code below:

func testNewTaskAPI() async throws {
    
    let task = sut.fetchUsersTaskTestable()
    let users = try await task.value
    
    XCTAssertTrue(users.count > 0)
}

And that’s it for today!  

 

Summary – Unit Testing Async/Await and SwiftUI

Today we discussed the challenges of unit testing asynchronous code in Swift and introduces the async/await syntax as a solution.

We explained that the traditional approach to unit testing asynchronous code in Swift involves using closures, but this can lead to complex and difficult-to-maintain tests. The async/await syntax, on the other hand, allows asynchronous code to be written and tested in a more straightforward and intuitive way.

The article covers three different approaches to unit testing asynchronous code in Swift with async/await: closure-based function unit testing, async function testing, and Task testing.

  1. The closure-based approach involves wrapping the asynchronous code in a closure and passing it to the withCheckedContinuation closure, which allows the test to wait for the asynchronous code to complete before evaluating the results.
  2. The async function testing approach involves writing the test itself as an async function, which allows the test to wait for the asynchronous code to complete using the await keyword.
  3. The Task testing approach involves using the class to return for a Task to complete, which can be helpful for testing code that uses the Task class from the Swift concurrency library.

 

Fellow Apple Lovers, that’s all. I hope you liked reading this article as much as I enjoyed writing it.  If you want to support this blog you can Buy Me a Coffee or say hello on Twitter. I’m available on LinkedIn, or send me an e-mail through the contact page.

You can likewise sponsor this blog so I can get my blog free of ad networks.

Thanks for the reading and… That’s all folks.

Source: Image