이 글은 누구를 위한 것인가
- XCTest의 장황함에 지친 iOS 개발자
- Swift Testing을 아직 사용해보지 않은 팀
- 파라미터화 테스트로 반복 코드를 줄이고 싶은 개발자
들어가며
XCTest는 Objective-C 시대부터 이어온 테스팅 프레임워크다. Swift가 발전하면서 func testSomething(), XCTAssertEqual, setUp() 패턴의 한계가 드러났다. Swift Testing은 Swift 5.9(Xcode 15)에서 소개되어 Swift 6에서 안정화된 현대적 대안이다.
이 글은 bluefoxdev.kr의 Swift 테스팅 전략 가이드 를 참고하고, Swift Testing 실전 마이그레이션 관점에서 확장하여 작성했습니다.
1. XCTest vs Swift Testing 비교
// XCTest (기존 방식)
import XCTest
class CartTests: XCTestCase {
var cart: ShoppingCart!
override func setUp() {
cart = ShoppingCart()
}
func testAddItem() {
cart.add(item: Item(id: "1", price: 10000))
XCTAssertEqual(cart.itemCount, 1)
}
func testTotalPrice() {
cart.add(item: Item(id: "1", price: 10000))
cart.add(item: Item(id: "2", price: 20000))
XCTAssertEqual(cart.totalPrice, 30000)
}
}
// Swift Testing (현대적 방식)
import Testing
@Suite("장바구니 테스트")
struct CartTests {
var cart = ShoppingCart() // setUp 불필요 — 각 테스트마다 새 인스턴스
@Test("아이템 추가")
mutating func addItem() {
cart.add(item: Item(id: "1", price: 10000))
#expect(cart.itemCount == 1)
}
@Test("총 가격 계산")
mutating func totalPrice() {
cart.add(item: Item(id: "1", price: 10000))
cart.add(item: Item(id: "2", price: 20000))
#expect(cart.totalPrice == 30000)
}
}
2. Swift Testing 핵심 기능
import Testing
// 파라미터화 테스트 — 여러 케이스를 하나로
@Test("가격 포맷 변환", arguments: [
(10000, "10,000원"),
(1000000, "1,000,000원"),
(0, "0원"),
(99, "99원"),
])
func priceFormatting(amount: Int, expected: String) {
#expect(formatPrice(amount) == expected)
}
// 비동기 테스트 — async/await 네이티브 지원
@Test("API 상품 로드")
func loadProductsAsync() async throws {
let service = ProductService(api: MockAPI())
let products = try await service.fetchProducts()
#expect(products.count > 0)
#expect(products.first?.id != nil)
}
// 에러 던지기 검증
@Test("재고 없을 때 오류")
func outOfStockError() throws {
let product = Product(id: "1", stock: 0)
#expect(throws: CartError.outOfStock) {
try cart.add(product: product, quantity: 1)
}
}
// 조건부 테스트
@Test(
"iOS 17 이상에서만 실행",
.enabled(if: {
if #available(iOS 17, *) { return true }
return false
}())
)
func iOS17Feature() {
// iOS 17+ 전용 기능 테스트
}
// 비활성화
@Test("작업 중인 테스트", .disabled("PROJ-1234 해결 후 활성화"))
func workInProgress() {
// 임시 비활성화
}
// 태그 기반 필터링
extension Tag {
@Tag static var networking: Self
@Tag static var ui: Self
@Tag static var core: Self
}
@Test("결제 API 호출", .tags(.networking))
func paymentAPICall() async throws { ... }
3. Suite 구성과 공유 컨텍스트
import Testing
// 중첩 Suite로 테스트 구조화
@Suite("사용자 인증")
struct AuthTests {
@Suite("로그인")
struct LoginTests {
@Test("올바른 자격증명")
func validCredentials() async throws {
let auth = AuthService(api: MockAuthAPI())
let token = try await auth.login(email: "test@test.com", password: "correct")
#expect(token != nil)
#expect(token!.isValid)
}
@Test("잘못된 비밀번호")
func wrongPassword() async throws {
let auth = AuthService(api: MockAuthAPI())
await #expect(throws: AuthError.invalidCredentials) {
_ = try await auth.login(email: "test@test.com", password: "wrong")
}
}
@Test("이메일 형식 오류")
func invalidEmail() throws {
let auth = AuthService(api: MockAuthAPI())
#expect(throws: AuthError.invalidEmail) {
try auth.validateEmail("not-an-email")
}
}
}
@Suite("토큰 관리")
struct TokenTests {
@Test("만료 토큰 갱신")
func refreshExpiredToken() async throws { ... }
@Test("로그아웃 시 토큰 삭제")
func clearTokenOnLogout() async throws { ... }
}
}
4. XCTest에서 마이그레이션
// 마이그레이션 전략: 공존 → 점진적 교체
// 기존 XCTest 파일은 그대로 두고,
// 새 파일은 Swift Testing으로 작성 (두 프레임워크 공존 가능)
// XCTest 코드를 Swift Testing으로 변환하는 패턴
// 변환 전 (XCTest)
func testUserCreation() throws {
let user = try User(name: "홍길동", email: "hong@test.com")
XCTAssertEqual(user.name, "홍길동")
XCTAssertFalse(user.isVerified)
XCTAssertNil(user.profileImage)
}
// 변환 후 (Swift Testing)
@Test("사용자 생성")
func userCreation() throws {
let user = try User(name: "홍길동", email: "hong@test.com")
#expect(user.name == "홍길동")
#expect(!user.isVerified)
#expect(user.profileImage == nil)
}
/*
변환 대응표:
XCTAssertEqual(a, b) → #expect(a == b)
XCTAssertTrue(x) → #expect(x)
XCTAssertFalse(x) → #expect(!x)
XCTAssertNil(x) → #expect(x == nil)
XCTAssertNotNil(x) → #expect(x != nil)
XCTAssertThrowsError(try f()) → #expect(throws: Error.self) { try f() }
XCTFail("메시지") → Issue.record("메시지")
*/
마무리
Swift Testing은 XCTest보다 훨씬 표현력이 좋고, 파라미터화 테스트로 중복 코드를 대폭 줄일 수 있다. #expect는 실패 시 실제 값을 자동으로 보여줘 디버깅도 편하다.
기존 XCTest를 모두 교체할 필요는 없다. 새로운 테스트는 Swift Testing으로 작성하고, 리팩터링 시 점진적으로 전환하는 전략이 현실적이다.