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
npx -y skills add almasumdev/awesome-ios-agent-skills --skill ios-testingAssembled 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
awaitdirectly in tests — both frameworks support it. - For timing, inject a clock or scheduler; don't sleep.
- Test cancellation by starting a
Taskand 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:
ArticlesTestsfor moduleArticles. - 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.sleepor 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.