Testing Mastery

Actor Isolation in Unit Tests

May 1, 2026
5 min read
Featured image for blog post: Actor Isolation in Unit Tests

Actors provide data isolation, but testing them requires understanding how actor isolation affects your test code.

Follow along with the code: iOS-Practice on GitHub

The Challenge

Actors protect state from concurrent access, but your tests need to interact with that state. XCTest runs on the main thread, and actor methods are isolated.

actor SearchManager {
    private let searchService: SearchService
    private var currentTask: Task<[String], Error>?

    init(searchService: SearchService) {
        self.searchService = searchService
    }

    func search(query: String) async throws -> [String] {
        currentTask?.cancel()

        let task = Task {
            try await Task.sleep(nanoseconds: 300_000_000)
            try Task.checkCancellation()
            return try await searchService.search(query: query)
        }

        currentTask = task
        return try await task.value
    }

    func cancelSearch() {
        currentTask?.cancel()
        currentTask = nil
    }
}

Testing Actor Methods

Actor methods are async by default when called from outside:

class SearchManagerTests: XCTestCase {
    var mockService: MockSearchService!
    var sut: SearchManager!

    override func setUp() {
        mockService = MockSearchService()
        sut = SearchManager(searchService: mockService)
    }

    func test_search_returnsResults() async throws {
        // Arrange
        mockService.resultsToReturn = ["Swift", "SwiftUI"]

        // Act - await is required for actor methods
        let results = try await sut.search(query: "swift")

        // Assert
        XCTAssertEqual(results, ["Swift", "SwiftUI"])
    }
}

Testing Cancellation

func test_search_cancelsPreviousSearch() async throws {
    // Arrange
    mockService.resultsToReturn = ["result"]

    // Act - Start multiple rapid searches
    Task { _ = try? await sut.search(query: "first") }
    Task { _ = try? await sut.search(query: "second") }

    try await Task.sleep(nanoseconds: 50_000_000)

    let results = try await sut.search(query: "final")

    // Assert - Only final search completes
    XCTAssertEqual(results, ["result"])
}

func test_cancelSearch_stopsPendingSearch() async {
    // Arrange
    mockService.searchDelay = 1.0 // Long delay

    // Act
    let searchTask = Task {
        try await sut.search(query: "test")
    }

    try? await Task.sleep(nanoseconds: 100_000_000)
    await sut.cancelSearch()

    // Assert
    do {
        _ = try await searchTask.value
        XCTFail("Expected cancellation")
    } catch {
        XCTAssertTrue(error is CancellationError)
    }
}

Mock for Actor Tests

class MockSearchService: SearchService {
    var resultsToReturn: [String] = []
    var errorToThrow: Error?
    var searchDelay: TimeInterval = 0
    private(set) var searchCallCount = 0

    func search(query: String) async throws -> [String] {
        searchCallCount += 1

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

        if let error = errorToThrow { throw error }
        return resultsToReturn
    }
}

@MainActor Test Classes

When testing @MainActor-isolated types:

@MainActor
class ViewModelTests: XCTestCase {
    var sut: SomeViewModel!

    override func setUp() async throws {
        sut = SomeViewModel()
    }

    func test_someMethod_updatesState() async {
        await sut.loadData()
        XCTAssertEqual(sut.items.count, 3)
    }
}

Key Patterns

ScenarioApproach
Calling actor methodsUse await in async test
Testing cancellationStart task, sleep briefly, cancel, verify error
@MainActor typesMark test class with @MainActor
Verifying isolationCheck that concurrent calls don't corrupt state

Actor tests are just async tests with the added guarantee that the actor handles isolation for you.

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