Ios testing
9 expert-level iOS development skills for Claude Code, Codex, and AI coding agents. SwiftUI, architecture, networking, data, security, concurrency, testing, accessibility, performance.
npx -y skills add koshkinvv/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.
One thing to look at
- 5 stars5 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.
What its author says it does
Copied from the file, not written here
iOS testing expert skill covering Swift Testing framework (@Test, #expect, #require, @Suite, parameterized tests, traits), XCTest (assertions, async testing, performance testing, XCTestExpectation), UI Testing (XCUIApplication, XCUIElement, Page Object pattern, accessibility identifiers), snapshot testing (swift-snapshot-testing), mocking strategies (protocol-based mocks, URLProtocol for network, test doubles), and testing patterns for SwiftUI, SwiftData, Combine, and async/await code. Use this skill whenever the user writes tests, creates test classes, needs mocking strategies, or asks about testing iOS code. Triggers on: test, @Test, #expect, XCTest, XCTestCase, unit test, UI test, integration test, mock, stub, spy, fake, snapshot test, test coverage, TDD, testing, assert, XCTAssert, Swift Testing, @Suite, parameterized test, test plan, test double, URLProtocol mock, ViewInspector, or any iOS testing question.
SKILL.md
11.2 KB, as published. Nobody here has run it
iOS Testing Skill
Core Rules
- Use Swift Testing (
@Test,#expect) for ALL new unit tests -- it is the modern framework (Xcode 16+). - Keep XCTest only for UI tests and performance tests -- Swift Testing does not support these yet.
- Both frameworks can coexist in the same target -- migrate incrementally, never rewrite working tests.
- Use protocol-based dependency injection for testability.
- Use
URLProtocolfor network mocking (NOT mocking URLSession directly). - Use
isStoredInMemoryOnly: truefor SwiftData test containers. - Name tests descriptively:
@Test("Login succeeds with valid credentials"). - One assertion per test is ideal, but pragmatic grouping is fine.
- Test behavior, not implementation details.
- Never use
sleep()in tests -- use expectations, confirmations, orClockinjection.
Framework Choice
| Test Type | Framework | Why |
|---|---|---|
| Unit tests (new) | Swift Testing | Modern, less boilerplate, parameterized tests |
| Unit tests (existing) | XCTest | Don't rewrite working tests without reason |
| UI tests | XCTest | Swift Testing doesn't support XCUIApplication |
| Performance tests | XCTest | measure {} not available in Swift Testing |
| Snapshot tests | XCTest + swift-snapshot-testing | Point-Free library, XCTest integration |
Test Organization
MyAppTests/ # Unit test target
Models/
UserTests.swift
OrderTests.swift
ViewModels/
LoginViewModelTests.swift
ProfileViewModelTests.swift
Services/
APIClientTests.swift
AuthServiceTests.swift
Helpers/
Mocks/
MockAPIClient.swift
MockAuthService.swift
TestData/
UserFixtures.swift
JSONFixtures.swift
MyAppUITests/ # UI test target
Screens/ # Page Objects
LoginScreen.swift
HomeScreen.swift
Flows/
OnboardingFlowTests.swift
PurchaseFlowTests.swift
Helpers/
XCUIApplication+Launch.swift
Quick Start: Swift Testing
import Testing
@testable import MyApp
@Suite("AuthService")
struct AuthServiceTests {
let sut: AuthService
let mockAPI: MockAPIClient
init() {
mockAPI = MockAPIClient()
sut = AuthService(api: mockAPI)
}
@Test("Login succeeds with valid credentials")
func loginSuccess() async throws {
mockAPI.loginResult = .success(User.fixture)
let user = try await sut.login(email: "[email protected]", password: "pass123")
#expect(user.email == "[email protected]")
#expect(mockAPI.loginCallCount == 1)
}
@Test("Login fails with invalid credentials")
func loginFailure() async {
mockAPI.loginResult = .failure(AuthError.invalidCredentials)
await #expect(throws: AuthError.invalidCredentials) {
try await sut.login(email: "[email protected]", password: "wrong")
}
}
@Test("Password validation", arguments: [
("short", false),
("validPass1!", true),
("nouppercase1!", false),
("NOLOWERCASE1!", false),
])
func passwordValidation(password: String, isValid: Bool) {
#expect(sut.isValidPassword(password) == isValid)
}
}
Quick Start: XCTest (UI Tests)
import XCTest
final class LoginUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments = ["--uitesting", "--reset-state"]
app.launch()
}
func test_login_withValidCredentials_showsHome() {
let loginScreen = LoginScreen(app: app)
loginScreen
.typeEmail("[email protected]")
.typePassword("password123")
.tapLogin()
let homeScreen = HomeScreen(app: app)
XCTAssertTrue(homeScreen.welcomeLabel.waitForExistence(timeout: 5))
}
}
Protocol-Based Mocking Pattern
// 1. Define protocol
protocol APIClientProtocol: Sendable {
func login(email: String, password: String) async throws -> User
func fetchProfile(id: String) async throws -> Profile
}
// 2. Production implementation
final class APIClient: APIClientProtocol {
func login(email: String, password: String) async throws -> User { /* real impl */ }
func fetchProfile(id: String) async throws -> Profile { /* real impl */ }
}
// 3. Mock for tests
final class MockAPIClient: APIClientProtocol, @unchecked Sendable {
var loginResult: Result<User, Error> = .failure(TestError.notConfigured)
var loginCallCount = 0
var loginReceivedArgs: [(email: String, password: String)] = []
func login(email: String, password: String) async throws -> User {
loginCallCount += 1
loginReceivedArgs.append((email, password))
return try loginResult.get()
}
var fetchProfileResult: Result<Profile, Error> = .failure(TestError.notConfigured)
var fetchProfileCallCount = 0
func fetchProfile(id: String) async throws -> Profile {
fetchProfileCallCount += 1
return try fetchProfileResult.get()
}
}
URLProtocol Network Mocking
final class MockURLProtocol: URLProtocol {
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
guard let handler = Self.requestHandler else {
client?.urlProtocolDidFinishLoading(self)
return
}
do {
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
}
// Usage in test:
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: config)
let apiClient = APIClient(session: session)
MockURLProtocol.requestHandler = { request in
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
let data = try JSONEncoder().encode(User.fixture)
return (response, data)
}
SwiftData Testing
@Test("Saving a user persists it")
func saveUser() throws {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: User.self, configurations: config)
let context = ModelContext(container)
let user = User(name: "Test", email: "[email protected]")
context.insert(user)
try context.save()
let descriptor = FetchDescriptor<User>()
let users = try context.fetch(descriptor)
#expect(users.count == 1)
#expect(users.first?.name == "Test")
}
Combine Testing
@Test("Publisher emits values correctly")
func publisherEmitsValues() async {
let viewModel = CounterViewModel()
var received: [Int] = []
let cancellable = viewModel.$count.sink { received.append($0) }
viewModel.increment()
viewModel.increment()
#expect(received == [0, 1, 2])
cancellable.cancel()
}
async/await Confirmation (Replaces XCTestExpectation)
@Test("Notification triggers callback")
func notificationCallback() async {
await confirmation("callback received") { confirm in
let observer = NotificationObserver {
confirm()
}
NotificationCenter.default.post(name: .testNotification, object: nil)
}
}
@Test("Delegate called exactly 3 times")
func delegateCalledThreeTimes() async {
await confirmation("delegate called", expectedCount: 3) { confirm in
let delegate = MockDelegate(onCall: { confirm() })
let sut = DataLoader(delegate: delegate)
await sut.loadBatch(count: 3)
}
}
Test Fixtures Pattern
extension User {
static var fixture: User {
User(id: "test-id", name: "Test User", email: "[email protected]")
}
static func fixture(
id: String = "test-id",
name: String = "Test User",
email: String = "[email protected]"
) -> User {
User(id: id, name: name, email: email)
}
}
Common Mistakes to Avoid
- Don't test private methods -- test public behavior instead.
- Don't mock what you don't own -- wrap third-party APIs in your own protocol.
- Don't use
sleep()orTask.sleep()for timing -- useClockinjection or expectations. - Don't share mutable state between tests -- use struct-based
@Suitewithinit(). - Don't forget
@MainActorisolation -- if your SUT is@MainActor, your test must be too. - Don't use
XCTAssertin Swift Testing -- use#expectand#require. - Don't force-unwrap in tests -- use
#requireorXCTUnwrap. - Don't test Apple frameworks -- trust that
UserDefaults.setworks. - Don't write tests after the fact just for coverage -- write tests that catch real bugs.
- Don't ignore flaky tests -- use
withKnownIssueto mark them, then fix root cause.
Migration: XCTest to Swift Testing
| XCTest | Swift Testing |
|---|---|
class MyTests: XCTestCase | @Suite struct MyTests |
func testSomething() | @Test func something() |
override func setUp() | init() |
override func tearDown() | deinit |
XCTAssertEqual(a, b) | #expect(a == b) |
XCTAssertNil(x) | #expect(x == nil) |
XCTAssertThrowsError(expr) | #expect(throws: ErrorType.self) { expr } |
XCTUnwrap(optional) | try #require(optional) |
XCTestExpectation + wait | confirmation { } |
XCTSkipIf(condition) | .enabled(if: !condition) trait |
measure { } | No equivalent -- keep in XCTest |
| UI tests with XCUIApplication | No equivalent -- keep in XCTest |
References
- references/swift-testing.md -- Swift Testing framework deep dive
- references/xctest.md -- XCTest assertions, async, performance
- references/ui-testing.md -- XCUIApplication, Page Object, accessibility
- references/mocking.md -- Protocol mocks, URLProtocol, snapshot testing