agentsclimarketplace

Ios testing

Skill almasumdev/awesome-ios-agent-skills/.github/skills/testing_and_automation/ios-testing

Expert guidance on iOS unit and snapshot testing with XCTest, the new Swift Testing framework, and swift-snapshot-testing. Use when writing or auditing tests.From its SKILL.md

Install
npx -y skills add almasumdev/awesome-ios-agent-skills --skill ios-testing

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

5.3 KB, ~1.2k tokens by cl100k_base, as published. Nobody here has run it

iOS Testing: XCTest, Swift Testing, Snapshots

Instructions

Two frameworks ship side by side:

  • XCTest — the established framework; still required for UI tests and for projects targeting older Xcode toolchains.
  • Swift Testing — the modern framework (Xcode 16+). Use it for unit tests in new modules.

Snapshot tests pin visual regressions. Prefer them over brittle widget-internal assertions.

1. Swift Testing (New)

import Testing
@testable import Articles

@Suite("ArticleListModel")
struct ArticleListModelTests {

    @Test func loadsArticlesOnAppear() async throws {
        let repo = StubArticleRepository(articles: [.sample])
        let model = ArticleListModel(repository: repo)
        await model.load()
        #expect(model.articles.count == 1)
        #expect(model.articles.first?.id == "sample")
    }

    @Test("surfaces repository errors")
    func load_failure() async throws {
        let repo = StubArticleRepository(error: DataError.offline)
        let model = ArticleListModel(repository: repo)
        await model.load()
        #expect(model.error as? DataError == .offline)
    }
}

Key primitives:

  • @Test — marks a function as a test.
  • @Suite — optional container; can carry setup/teardown via init/deinit.
  • #expect(...) — non-fatal assertion; #require(...) — fatal.
  • @Test(arguments:) — parameterized tests.
  • .tags(...) — group and filter by tag.

Parameterized example:

@Test(arguments: [0, 1, 5, 100])
func formatsCount(_ n: Int) {
    #expect(!Counter.format(n).isEmpty)
}

2. XCTest (Still Supported)

import XCTest
@testable import Articles

final class ArticleListModelTests: XCTestCase {

    func test_load_populatesArticles() async throws {
        let repo = StubArticleRepository(articles: [.sample])
        let model = ArticleListModel(repository: repo)
        await model.load()
        XCTAssertEqual(model.articles.count, 1)
    }
}

3. Test Doubles

Prefer protocol-based stubs. Mocking frameworks are rarely needed in Swift — hand-written stubs are clearer and faster.

struct StubArticleRepository: ArticleRepository {
    var articles: [Article] = []
    var error: Error?
    func latest() async throws -> [Article] {
        if let error { throw error }
        return articles
    }
    func refresh() async throws {}
    func observeLatest() -> AsyncStream<[Article]> { .init { $0.finish() } }
}

4. Snapshot Tests

Add swift-snapshot-testing as an SPM dependency. Pin views and screens at critical sizes and Dynamic Type levels:

import SnapshotTesting
import SwiftUI
import XCTest
@testable import ArticlesUI

final class ArticleRowSnapshotTests: XCTestCase {
    override func invokeTest() {
        withSnapshotTesting(record: .missing) { super.invokeTest() }
    }

    func test_row_default() {
        let view = ArticleRow(article: .sample)
            .frame(width: 375)
            .fixedSize(horizontal: false, vertical: true)
        assertSnapshot(of: view, as: .image)
    }

    func test_row_XXL() {
        let view = ArticleRow(article: .sample)
            .environment(\.dynamicTypeSize, .accessibility3)
            .frame(width: 375)
        assertSnapshot(of: view, as: .image, named: "xxl")
    }
}

Commit the reference images. Diff previews appear inline on failure.

5. Async Testing Patterns

  • Use await directly in tests — both frameworks support it.
  • For timing, inject a clock or scheduler; don't sleep.
  • Test cancellation by starting a Task and cancelling it, then asserting the observable result (isCancelled, final state).
@Test func cancellation_marksLoading() async {
    let model = LongRunningModel()
    let task = Task { await model.work() }
    task.cancel()
    await task.value
    #expect(model.state == .cancelled)
}

6. Parallel Execution

Both frameworks default to parallel execution. Ensure tests do not share mutable global state — filesystem paths, UserDefaults, Keychain, singletons. Use unique suite IDs and temporary directories.

let tempDir = URL.temporaryDirectory.appendingPathComponent(UUID().uuidString)

7. Coverage

Enable Code Coverage in the scheme's Test action. Don't chase 100% — focus on domain, repository, and view-model logic.

8. Naming and Structure

  • Mirror the production module structure: ArticlesTests for module Articles.
  • One test file per production type.
  • Test names describe behavior: fetchLatest_returnsCache_whenOffline.

9. CI

  • Run on a clean simulator per job.
  • Fail fast on snapshot diffs but commit the attachment so reviewers can see changes.
  • Publish coverage to the CI dashboard (see ios-ci-cd).

Checklist

  • New modules use Swift Testing when Xcode 16+ is available.
  • Tests use hand-written stubs over mocking frameworks.
  • Critical screens have snapshot coverage at default and XXL Dynamic Type.
  • No Thread.sleep or real clocks in tests — inject dependencies.
  • Tests are safely parallel; no shared singletons.
  • Tests run in CI on a clean simulator.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,764. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.