Testing Mastery

Testing Async/Await Methods

April 22, 2026
5 min read
Featured image for blog post: Testing Async/Await Methods

XCTest fully supports async/await. Here's how to write clean, reliable tests for your async code.

Follow along with the code: iOS-Practice on GitHub

Async Test Functions

Mark your test function as async and you can await directly:

func test_getWeather_callsService() async throws {
    // Act
    _ = try await sut.getWeather(for: "London")

    // Assert
    XCTAssertEqual(mockService.fetchCurrentWeatherCallCount, 1)
    XCTAssertEqual(mockService.requestedCities, ["London"])
}

No callbacks, no expectations, no completion handlers. Just clean, sequential code.

Mock Async Services

class MockWeatherService: WeatherService {
    var weatherToReturn: Weather?
    var errorToThrow: Error?
    var fetchDelay: TimeInterval = 0

    private(set) var fetchCurrentWeatherCallCount = 0
    private(set) var requestedCities: [String] = []

    func fetchCurrentWeather(for city: String) async throws -> Weather {
        fetchCurrentWeatherCallCount += 1
        requestedCities.append(city)

        if fetchDelay > 0 {
            try await Task.sleep(nanoseconds: UInt64(fetchDelay * 1_000_000_000))
        }

        if let error = errorToThrow { throw error }

        return weatherToReturn ?? Weather(
            city: city,
            temperature: 72,
            condition: "Sunny",
            humidity: 50,
            date: Date()
        )
    }

    func reset() {
        weatherToReturn = nil
        errorToThrow = nil
        fetchDelay = 0
        fetchCurrentWeatherCallCount = 0
        requestedCities = []
    }
}

Testing Error Propagation

func test_getWeather_whenServiceThrows_propagatesError() async {
    // Arrange
    mockService.errorToThrow = WeatherError.cityNotFound

    // Act & Assert
    do {
        _ = try await sut.getWeather(for: "FakeCity")
        XCTFail("Expected error to be thrown")
    } catch {
        XCTAssertEqual(error as? WeatherError, .cityNotFound)
    }
}

Testing with Delays

Sometimes you need to simulate slow responses:

func test_getWeather_withSlowService_completesEventually() async throws {
    // Arrange
    mockService.fetchDelay = 0.5 // 500ms delay

    // Act - This should complete despite the delay
    let weather = try await sut.getWeather(for: "SlowCity")

    // Assert
    XCTAssertEqual(weather.city, "SlowCity")
}

Test Setup with @MainActor

If your ViewModel is @MainActor, your tests need to be too:

@MainActor
class WeatherViewModelTests: XCTestCase {
    var mockService: MockWeatherService!
    var sut: WeatherViewModel!

    override func setUp() async throws {
        mockService = MockWeatherService()
        sut = WeatherViewModel(service: mockService)
    }

    func test_loadWeather_updatesState() async {
        mockService.weatherToReturn = Weather(city: "NYC", ...)

        await sut.loadWeather(for: "NYC")

        XCTAssertEqual(sut.currentWeather?.city, "NYC")
    }
}

Key Points

PatternUse Case
async throws testMost async tests
Mock with delayTesting timeout behavior
@MainActor test classTesting MainActor-isolated code
do/catch in testVerifying specific errors

Async testing in Swift is straightforward—just mark the test async and await your calls.

Originally published on pixelper.com

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch