Section 10/171 menit
10. Testing di Xcode Cloud
10. Testing di Xcode Cloud
Konfigurasi Test di Workflow
Xcode Cloud mendukung unit test dan UI test. Untuk hasil terbaik:
swift
Test Action Configuration:
├── Scheme: MyApp
├── Test Plan: AllTests.xctestplan (pisahkan unit dan UI test)
├── Destinations:
│ ├── iPhone 15 Pro — iOS 17.x (simulator terbaru)
│ ├── iPhone SE (3rd generation) — iOS 17.x (test device kecil)
│ └── iPad Pro 12.9-inch — iPadOS 17.x (jika support iPad)
└── Options:
├── Code Coverage: Enabled
└── Test Repetition: None (atau Fixed — untuk flaky test debugging)
Memisahkan Unit Test dan UI Test
UI test lebih lambat dan lebih flaky — pisahkan ke workflow berbeda:
swift
Workflow "PR Validation":
└── Test Action: scheme MyApp, test plan UnitTests.xctestplan
(cepat, ~2-5 menit, jalankan di setiap PR)
Workflow "Nightly":
└── Test Action: scheme MyApp, test plan AllTests.xctestplan
(termasuk UI test, ~15-30 menit, jalankan setiap malam)
Swift Testing di Xcode Cloud
Swift Testing (diperkenalkan Swift 5.9) bekerja seamless di Xcode Cloud:
swift
// Tests/UserServiceTests.swift
import Testing
@testable import MyApp
@Suite("UserService Tests")
struct UserServiceTests {
@Test("Login berhasil dengan kredensial valid")
func loginSuccess() async throws {
let service = UserService(apiClient: MockAPIClient())
let user = try await service.login(
email: "test@example.com",
password: "password123"
)
#expect(user.email == "test@example.com")
#expect(user.isLoggedIn == true)
}
@Test("Login gagal dengan password salah", arguments: [
("wrong@email.com", "password123"),
("test@example.com", "wrongpassword"),
])
func loginFailure(email: String, password: String) async {
let service = UserService(apiClient: MockAPIClient())
await #expect(throws: AuthError.invalidCredentials) {
try await service.login(email: email, password: password)
}
}
// Tag untuk grouping di Xcode Cloud test results
@Test("Network timeout handling", .tags(.networking))
func networkTimeout() async {
let client = MockAPIClient()
client.shouldTimeout = true
let service = UserService(apiClient: client)
await #expect(throws: NetworkError.timeout) {
try await service.login(email: "test@example.com", password: "pass")
}
}
}
extension Tag {
@Tag static var networking: Self
}
Menangani Flaky Tests
Xcode Cloud punya fitur Test Repetitions untuk menangani test flaky:
swift
Test Action → Options → Test Repetition Mode:
├── None (default)
├── Until Failure — jalankan berulang sampai gagal (debugging)
└── Retry on Failure — coba ulang N kali jika gagal (CI tolerance)
└── Maximum Repetitions: 3
Untuk jangka panjang, identifikasi dan perbaiki test flaky daripada mengandalkan retry:
swift
// Tests/NetworkTests.swift
// ❌ Test flaky — bergantung pada timing
@Test func networkCallCompletesInTime() async {
let start = Date()
_ = try? await apiClient.fetchUser(id: 1)
let elapsed = Date().timeIntervalSince(start)
#expect(elapsed < 2.0) // flaky jika server lambat
}
// ✓ Test deterministic — gunakan mock
@Test func networkCallReturnsUser() async throws {
let mockClient = MockAPIClient()
mockClient.mockResponse = User(id: 1, name: "Alice")
let user = try await mockClient.fetchUser(id: 1)
#expect(user.name == "Alice")
}
Code Coverage Report
Xcode Cloud otomatis generate code coverage report yang bisa dilihat di Xcode atau App Store Connect:
swift
Build Results → Test → Code Coverage:
├── Overall: 78%
├── UserService.swift: 92%
├── NetworkClient.swift: 65%
└── ProfileViewController.swift: 43% ← butuh lebih banyak test
Untuk enforce minimum coverage, gunakan ci_post_xcodebuild.sh:
swift
#!/bin/zsh
# ci_scripts/ci_post_xcodebuild.sh
if [[ "$CI_XCODEBUILD_ACTION" == "test" ]]; then
MINIMUM_COVERAGE=70 # persen minimum
# Extract coverage dari xcresult menggunakan xcov atau xcresulttool
# (contoh konseptual — implementasi aktual bergantung pada tool yang digunakan)
ACTUAL_COVERAGE=$(xcrun xcresulttool get \
--format json \
--path "$CI_RESULT_BUNDLE_PATH" 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); \
print(int(d.get('metrics',{}).get('testsCount',{}).get('_value',0)))" \
|| echo "0")
echo "Coverage: ${ACTUAL_COVERAGE}%"
if (( ACTUAL_COVERAGE < MINIMUM_COVERAGE )); then
echo "❌ Code coverage ${ACTUAL_COVERAGE}% di bawah minimum ${MINIMUM_COVERAGE}%"
exit 1
fi
fi