| name | mobiai-ios-testing |
| description | Use when writing or running tests in an iOS project — unit tests, UI tests, snapshot tests, choosing the right framework. |
| license | MIT |
| compatibility | ["claude-code","cursor","copilot","codex"] |
| platforms | ["ios"] |
iOS Testing
Community contribution welcome! Help improve this skill with your XCTest patterns, mocking strategies, and testing tips.
Guide to writing and running tests in iOS projects.
When to Use
- Writing unit tests for Swift code
- Setting up test infrastructure
- Running and debugging test suites
Test Types
| Type | Target | Framework | Speed |
|---|
| Unit | MyAppTests | XCTest | Fast |
| UI | MyAppUITests | XCUITest | Slow |
| Snapshot | MyAppTests | SnapshotTesting (pointfree) | Medium |
Unit Testing (XCTest)
File Location
- Source:
MyApp/Feature/MyViewModel.swift
- Test:
MyAppTests/Feature/MyViewModelTests.swift
Test Structure
import XCTest
@testable import MyApp
final class MyViewModelTests: XCTestCase {
private var sut: MyViewModel!
private var mockRepository: MockRepository!
override func setUp() {
super.setUp()
mockRepository = MockRepository()
sut = MyViewModel(repository: mockRepository)
}
override func tearDown() {
sut = nil
mockRepository = nil
super.tearDown()
}
func testLoadData_setsStateToSuccess_whenRepositoryReturnsData() async {
mockRepository.stubbedResult = [Item(id: "1", name: "Test")]
await sut.loadData()
XCTAssertEqual(sut.state, .success([Item(id: "1", name: "Test")]))
}
func testLoadData_setsStateToError_whenRepositoryThrows() async {
mockRepository.stubbedError (domain: , code: )
sut.loadData()
.error sut.state {
} {
()
}
}
}
Naming Conventions
func testCalculateTotal_returnsZero_whenCartIsEmpty() { ... }
func testFormatPrice_throws_whenAmountIsNegative() { ... }
Async Testing
func testFetchData_returnsItems() async throws {
let items = try await sut.fetchData()
XCTAssertFalse(items.isEmpty)
}
func testPublisher_emitsValue() {
let expectation = expectation(description: "Value emitted")
var received: String?
sut.publisher
.sink { value in
received = value
expectation.fulfill()
}
.store(in: &cancellables)
sut.trigger()
wait(for: [expectation], timeout: 1.0)
XCTAssertEqual(received, "expected")
}
Manual Mocking (No Framework)
class MockRepository: RepositoryProtocol {
var stubbedResult: [Item] = []
var stubbedError: Error?
var fetchDataCallCount = 0
func fetchData() async throws -> [Item] {
fetchDataCallCount += 1
if let error = stubbedError { throw error }
return stubbedResult
}
}
XCUITest (UI Testing)
final class MyAppUITests: XCTestCase {
let app = XCUIApplication()
override func setUp() {
super.setUp()
continueAfterFailure = false
app.launch()
}
func testLogin_navigatesToHome() {
app.textFields["Email"].tap()
app.textFields["Email"].typeText("test@test.com")
app.secureTextFields["Password"].tap()
app.secureTextFields["Password"].typeText("password")
app.buttons["Login"].tap()
XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 5))
}
}
Running Tests
xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15 Pro' -quiet
xcodebuild test -scheme MyApp -destination '...' -only-testing:MyAppTests/MyViewModelTests
xcodebuild test -scheme MyApp -destination '...' -only-testing:MyAppTests/MyViewModelTests/testLoadData_setsStateToSuccess
xcodebuild test -scheme MyApp -destination '...' -only-testing:MyAppUITests
Quick/Nimble (BDD Style)
import Quick
import Nimble
@testable import MyApp
class MyViewModelSpec: QuickSpec {
override class func spec() {
describe("MyViewModel") {
var sut: MyViewModel!
beforeEach { sut = MyViewModel() }
context("when data loads successfully") {
it("sets state to success") {
expect(sut.state).toEventually(equal(.success))
}
}
}
}
}
Want to improve this skill? Add your testing patterns, mocking strategies, and CI tips via a PR.