| name | swift-testing |
| description | Test Swift applications - XCTest, Swift Testing, UI tests, mocking, TDD, CI/CD |
| version | 2.0.0 |
| sasmp_version | 1.3.0 |
| bonded_agent | 06-swift-testing |
| bond_type | PRIMARY_BOND |
Swift Testing Skill
Comprehensive testing strategies for Swift applications using XCTest and Swift Testing framework.
Prerequisites
- Xcode 15+ installed
- Understanding of dependency injection
- Familiarity with async/await
Parameters
parameters:
framework:
type: string
enum: [xctest, swift_testing]
default: swift_testing
test_type:
type: string
enum: [unit, integration, ui, snapshot]
default: unit
coverage_target:
type: number
default: 80
description: Target code coverage percentage
ci_platform:
type: string
enum: [xcode_cloud, github_actions, gitlab_ci, none]
default: github_actions
Topics Covered
Test Frameworks
| Framework | Min Version | Key Features |
|---|
| XCTest | iOS 2.0+ | XCTestCase, expectations |
| Swift Testing | iOS 17+ / Swift 5.9+ | @Test, #expect, traits |
Test Types
| Type | Scope | Speed |
|---|
| Unit | Single function/class | Fastest |
| Integration | Multiple components | Medium |
| UI | Full user flows | Slowest |
| Snapshot | Visual regression | Medium |
Testing Patterns
| Pattern | Purpose |
|---|
| AAA | Arrange, Act, Assert |
| Given-When-Then | BDD style |
| Test Doubles | Mock, Stub, Spy, Fake |
Code Examples
Swift Testing (iOS 17+ / Swift 5.9+)
import Testing
@testable import MyApp
@Suite("ShoppingCart Tests")
struct ShoppingCartTests {
var cart: ShoppingCart
var mockRepository: MockProductRepository
init() {
mockRepository = MockProductRepository()
cart = ShoppingCart(repository: mockRepository)
}
@Test("adding product increases count")
func addProduct() async throws {
let product = Product(id: "1", name: "Widget", price: 9.99)
cart.add(product)
#expect(cart.items.count == 1)
#expect(cart.items.first?.product == product)
}
@Test("adding same product increases quantity")
func addSameProductTwice() {
let product = Product(id: "1", name: "Widget", price: 9.99)
cart.add(product)
cart.add(product)
#expect(cart.items.count == 1)
#expect(cart.items.first?.quantity == 2)
}
@Test()
() {
cart.add((id: , name: , price: ))
cart.add((id: , name: , price: ))
#expect(cart.total )
}
(, .tags(.checkout))
() {
#expect(throws: .empty) {
cart.checkout()
}
}
(, .tags(.checkout))
() {
cart.add((id: , name: , price: ))
mockRepository.checkoutResult .success((id: ))
order cart.checkout()
#expect(order.id )
#expect(cart.items.isEmpty)
}
(arguments: [, , , ])
(: ) {
discount cart.calculateDiscount(forQuantity: quantity)
quantity {
: #expect(discount )
: #expect(discount )
: #expect(discount )
}
}
}
XCTest with Async
import XCTest
@testable import MyApp
final class ProductServiceTests: XCTestCase {
var sut: ProductService!
var mockAPI: MockAPIClient!
override func setUp() {
super.setUp()
mockAPI = MockAPIClient()
sut = ProductService(api: mockAPI)
}
override func tearDown() {
sut = nil
mockAPI = nil
super.tearDown()
}
func test_fetchProducts_success() async throws {
let expectedProducts = [Product(id: "1", name: "Test", price: 9.99)]
mockAPI.productsResult = .success(expectedProducts)
let products = try await sut.fetchProducts()
XCTAssertEqual(products, expectedProducts)
XCTAssertTrue(mockAPI.fetchProductsCalled)
}
func test_fetchProducts_networkError_throws() {
mockAPI.productsResult .failure(.noConnection)
{
sut.fetchProducts()
()
} {
(error )
}
}
() {
attempts
mockAPI.onFetchProducts {
attempts
attempts {
.timeout
}
[(id: , name: , price: )]
}
sut.fetchProductsWithRetry(maxAttempts: )
(attempts, )
}
}
Mock Implementation
protocol APIClientProtocol {
func fetchProducts() async throws -> [Product]
func createOrder(_ order: CreateOrderRequest) async throws -> Order
}
final class APIClient: APIClientProtocol {
func fetchProducts() async throws -> [Product] {
}
func createOrder(_ order: CreateOrderRequest) async throws -> Order {
}
}
final class MockAPIClient: APIClientProtocol {
var productsResult: Result<[Product], Error> = .success([])
var orderResult: Result<Order, Error> = .success(Order(id: ))
fetchProductsCalled
fetchProductsCallCount
createOrderCalled
lastOrderRequest: ?
onFetchProducts: (() -> [])
() -> [] {
fetchProductsCalled
fetchProductsCallCount
handler onFetchProducts {
handler()
}
productsResult.get()
}
( : ) -> {
createOrderCalled
lastOrderRequest order
orderResult.get()
}
() {
productsResult .success([])
orderResult .success((id: ))
fetchProductsCalled
fetchProductsCallCount
createOrderCalled
lastOrderRequest
onFetchProducts
}
}
UI Testing with Page Object Pattern
import XCTest
struct LoginPage {
let app: XCUIApplication
var usernameField: XCUIElement {
app.textFields["username"]
}
var passwordField: XCUIElement {
app.secureTextFields["password"]
}
var loginButton: XCUIElement {
app.buttons["login"]
}
var errorMessage: XCUIElement {
app.staticTexts["errorMessage"]
}
func login(username: String, password: String) {
usernameField.tap()
usernameField.typeText(username)
passwordField.tap()
passwordField.typeText(password)
loginButton.tap()
}
func waitForLogin(timeout: TimeInterval = 5) -> Bool {
!usernameField.waitForExistence(timeout: timeout)
}
}
final class LoginUITests: XCTestCase {
var app: XCUIApplication!
var loginPage: LoginPage!
override func setUp() {
super.setUp()
continueAfterFailure =
app ()
app.launchArguments [, ]
app.launch()
loginPage (app: app)
}
() {
loginPage.login(username: , password: )
(loginPage.waitForLogin())
(app.tabBars[].exists)
}
() {
loginPage.login(username: , password: )
(loginPage.errorMessage.waitForExistence(timeout: ))
(loginPage.errorMessage.label, )
}
}
GitHub Actions CI
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.2.app
- name: Build and Test
run: |
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=17.2' \
-resultBundlePath TestResults.xcresult \
-enableCodeCoverage YES \
CODE_SIGNING_ALLOWED=NO
- name: Upload Results
uses: actions/upload-artifact@v3
if: failure()
with:
name: test-results
path: TestResults.xcresult
- name: Coverage Report
run:
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|
| Flaky tests | Shared state | Add setUp/tearDown cleanup |
| Async timeout | Missing fulfillment | Call fulfill() or increase timeout |
| UI element not found | Wrong identifier | Check accessibilityIdentifier |
| Mock not working | Wrong initialization | Verify dependency injection |
| Coverage low | Untested paths | Add edge case tests |
Debug Tips
print(app.debugDescription)
let exists = element.waitForExistence(timeout: 5)
let screenshot = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.lifetime = .keepAlways
add(attachment)
Validation Rules
validation:
- rule: test_naming
severity: info
check: Use descriptive test names (test_method_condition_result)
- rule: one_assertion
severity: info
check: Prefer one logical assertion per test
- rule: no_test_interdependence
severity: error
check: Tests must not depend on each other
Usage
Skill("swift-testing")
Related Skills
swift-fundamentals - Code to test
swift-concurrency - Testing async code
swift-architecture - Testable architecture