Jacob’s Tech Tavern

Jacob’s Tech Tavern

Unit Testing with async/await

Write robust and maintainable software using modern language features

Jacob Bartlett's avatar
Jacob Bartlett
Jul 18, 2023

Welcome to Part III of our async testing epic!

After comprehending the ancient wisdom of dependency injection; and adding the mysteries of mocking to your toolbelt; our first dip into asynchronous unit testing will feel like a breeze.


  1. Part I Dependency Injection Demystified

  2. Part II Mocking like a Pro

  3. Part III Unit Testing with async/await

  4. Part IV Advanced async testing: Unstructured concurrency

  5. Part V (interlude) - Implementing the Repository pattern with Combine

  6. Part VI Combine, async/await, and testing


Part III - Unit Testing Async Code

Let's harness our freshly-acquired skills to write our first async tests.

Fortunately, async/await is specially designed to make asynchronous code look and feel exactly the same as synchronous code - so our first tests with them are very straightforward.

We're going to test 3 common behaviours scenarios in this post:

  • Is my code properly talking to the dependencies it depends on?

  • How is my code dealing with successful function calls?

  • How is my code handling error states that result from function calls?

Setting up our first tests with callCount

As I mentioned in Part 2, we’ve set our mocks up to count the number of times each method is called, like a jobsworth club bouncer and his clicker. Some of the simplest tests are callCount tests - we’re essentially asking our code to answer this question:

When I call this function, does the service on which my class depends get called?

Here's the first such test: found in the BeerViewModelTests in my Bev project.

Let's step through the code line-by-line to make sure we all understand:

1. In Swift's XCTest, you need to prefix any test method with test. We're also marking this function async because it contains an asynchronous method call. If you leave this out, XCode flags up a handly compiler error.

2. We've set our mockBeerRepository up to fail if we call any methods without setting a stub (either a value or an error) - so here, we simply tell it to return an empty array to us when we call loadBeers.

3. This is where the magic happens! We call loadBeers() on the system under test - AKA the sut - our instance of BeerViewModel. Since this is an async method, we prefix with await.

4. Finally, to make this a proper test, we need to make assertions. Since we called sut.loadBeers() once, it should have in turn called loadBeers() on the repository. Therefore, we expect that the mock's loadBeersCallCount property is equal to 1.

Now we've understood our first test, writing another similar test in the BeerRepositoryTests is quite straightforward:

These look very similar to the view model - except that we're using a dependency-injected mock of the BeerAPI; and our sut is the Repository this time around.

If you like, take a proper look at the full files for BeerViewModelTests and BeerRepositoryTests to see the full setup logic and take another look at the mocks we're setting up.

Testing succces and failure states

These tests are the real meat-and-potatoes of unit testing. We're going to focus on writing tests for the `BeerAPI` in this section - check out BeerAPITests in my sample project for the full code.

We often start writing our code to deal with the happy path - when everything goes according to plan - we get the data we want, and process it successfully.

Of course, unfortunately, we live in the real world.

Perhaps we do simply get the data we want. But oftentimes, a user is offline. Our function could succeed, but find no actual data. Finally, we need to expect the unexpected - and handle unexpected error cases.

If we have tests which verify how our code deals with all the possible scenarios, then we have confidence that the code we're shipping to our users is robust and maintainable.

In BeerAPI, the getBeers() method asks our URLSessionProtocol to request the beer data from the network at the api.punkapi.com endpoint. The URLSessionProtocol is URLSession.default in our production code, but our tests set it up with the MockURLSession.

The function signature getBeers() looks like this:

public func getBeers() async throws -> [Beer]

This tells us it's asynchronous, that we can expect it to return an array of beers, and finally that it might throw an error for us to handle.

Subscribe for free to continue reading

Enter your email and get every post from Jacob’s Tech Tavern.
Already have an account? Sign in
© 2026 Jacob Bartlett · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for writing, podcasting, and media founders.