Unit testing with XCTest and mocks

Sofia Martinez Jan 2026
2 tabs
import XCTest
import Combine
@testable import MyApp

class PostsViewModelTests: XCTestCase {
    var viewModel: PostsViewModel!
    var mockNetworkService: MockNetworkService!
    var cancellables: Set<AnyCancellable>!

    override func setUp() {
        super.setUp()
        mockNetworkService = MockNetworkService()
        viewModel = PostsViewModel(networkService: mockNetworkService)
        cancellables = []
    }

    override func tearDown() {
        viewModel = nil
        mockNetworkService = nil
        cancellables = nil
        super.tearDown()
    }

    func testLoadPostsSuccess() {
        // Arrange
        let expectedPosts = [
            Post(id: 1, title: "Test Post", body: "Body", authorId: 1)
        ]
        mockNetworkService.postsToReturn = expectedPosts

        let expectation = XCTestExpectation(description: "Posts loaded")

        viewModel.$posts
            .dropFirst() // Skip initial empty state
            .sink { posts in
                XCTAssertEqual(posts.count, 1)
                XCTAssertEqual(posts.first?.title, "Test Post")
                expectation.fulfill()
            }
            .store(in: &cancellables)

        // Act
        viewModel.loadPosts()

        // Assert
        wait(for: [expectation], timeout: 1.0)
        XCTAssertFalse(viewModel.isLoading)
        XCTAssertNil(viewModel.errorMessage)
    }

    func testLoadPostsFailure() {
        // Arrange
        mockNetworkService.shouldFail = true

        let expectation = XCTestExpectation(description: "Error received")

        viewModel.$errorMessage
            .dropFirst()
            .sink { error in
                if error != nil {
                    expectation.fulfill()
                }
            }
            .store(in: &cancellables)

        // Act
        viewModel.loadPosts()

        // Assert
        wait(for: [expectation], timeout: 1.0)
        XCTAssertNotNil(viewModel.errorMessage)
        XCTAssertTrue(viewModel.posts.isEmpty)
    }

    func testSearchDebounce() {
        // Test that search is debounced
        let expectation = XCTestExpectation(description: "Search executed once")
        expectation.expectedFulfillmentCount = 1

        mockNetworkService.searchCallCount = 0

        // Act - rapid changes
        viewModel.searchQuery = "t"
        viewModel.searchQuery = "te"
        viewModel.searchQuery = "tes"
        viewModel.searchQuery = "test"

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            // Assert - only one search call after debounce
            XCTAssertEqual(self.mockNetworkService.searchCallCount, 1)
            expectation.fulfill()
        }

        wait(for: [expectation], timeout: 1.0)
    }
}
2 files · swift Explain with highlit

Unit tests verify code behavior in isolation using XCTest framework. I create test classes inheriting from XCTestCase with test prefixed methods. Each test has arrange-act-assert structure: set up dependencies, execute code, verify results with XCTAssert methods. For testing ViewModels, I inject mock dependencies through initializers to control external behavior. Mocks implement protocols, returning predetermined values or tracking method calls. Async tests use expectations—create with expectation(description:), fulfill when async completes, wait with waitForExpectations. Test-driven development guides design toward testable, decoupled code. Running tests frequently catches regressions early.