WWDC24 Swift Testing으로 테스트 심화

Ios_Roy·2025년 10월 16일

WWDC

목록 보기
8/13
post-thumbnail

1. 도입: 왜 테스트를 작성하는가?

  • 테스트는 코드 품질 확보의 핵심 단계입니다.
  • 자동화된 테스트로 사용자에게 전달되기 전에 문제를 발견하고, 에지 케이스까지 신뢰감 있게 다룹니다.

테스트가 해결하는 도전 과제들:

  • 더 복잡해진 프로젝트에서 테스트 유지·관리가 힘들다.
  • 테스트 코드도 읽고 이해하기 쉬워야 한다.
  • 대규모 테스트 모음의 구조화·관계 관리가 필요하다.

2. Swift Testing 핵심: 표현력 있는 테스트 작성

기대치(Expectation): #expect, #require

  • #expect 매크로는 단순한 참/거짓 검증 외에도 다양한 복잡한 검증을 간결하게 처리합니다.
  • 예: 성공 케이스, 오류 발생, 특정 오류 타입/상세 등

기본 사용 예시

import Testing
@Test func brewTeaSuccessfully() throws {
    let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4)
    let cupOfTea = try teaLeaves.brew(forMinutes: 3)
    #expect(cupOfTea.quality == .perfect)
}

오류 발생 확인

import Testing
@Test func brewTeaError() throws {
    let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4)
    #expect(throws: BrewingError.self) {
        try teaLeaves.brew(forMinutes: 200)
    }
}
  • 특정 오류 유형만 통과시키려면 타입 전달: BrewingError.self

상세 오류값까지 검증하기

#expect { try teaLeaves.brew(forMinutes: 3) } throws: { error in
    guard let error = error as? BrewingError,
          case let .needsMoreTime(optimalBrewTime) = error else { return false }
    return optimalBrewTime == 4
}

필수 기대치(#require)

  • 테스트 흐름 중 반드시 성립해야 할 조건에는 #require를 사용
  • 실패하면 테스트를 즉시 종료, 귀찮은 nil 체크 등을 간소화

3. 알려진 문제(known issue) 관리하기

  • 외부 상태나 환경 문제 등, 일시적으로 실패가 허용되는 테스트는 withKnownIssue { ... }로 감싸 관리
    • 테스트 결과상 실패가 아닌 "알려진 이슈"로 표시됨
import Testing
@Test func softServeIceCreamInCone() throws {
    withKnownIssue {
        try softServeMachine.makeSoftServe(in: .cone)
    }
}

4. 테스트 설명 커스터마이징 (CustomTestStringConvertible)

  • 값 타입(Struct, Enum 등)이 테스트 중 불필요하게 긴 출력을 할 경우, CustomTestStringConvertible 프로토콜을 구현하면 가독성이 큰 폭 개선된다.
struct SoftServe: CustomTestStringConvertible {
    let flavor: Flavor
    let container: Container
    var testDescription: String { "\(flavor) in a \(container)" }
}

5. 매개변수화 테스트(Parameterize Tests)로 커버리지 확대

기존 방식 문제점

  • 각 Enum 케이스별, 인풋값별로 개별 함수 작성 → 중복 & 유지보수 부담

새 방식: @Test(arguments: …)

  • 하나의 테스트 함수에 컬렉션·시퀀스를 전달하여, 각 값마다 하나의 테스트 결과로 자동 실행
  • 두 개까지 컬렉션을 인수로 받을 수 있어, 모든 조합 테스트도 자동화

단일 인수 예시

enum Ingredient: CaseIterable { case rice, potato, lettuce, egg }
@Test(arguments: Ingredient.allCases)
func cook(_ ingredient: Ingredient) async throws {
    #expect(ingredient.isFresh)
    let result = try cook(ingredient)
    try #require(result.isDelicious)
}

다중 인수 예시

enum Ingredient: CaseIterable { ... }
enum Dish: CaseIterable { ... }
@Test(arguments: Ingredient.allCases, Dish.allCases)
func cook(_ ingredient: Ingredient, into dish: Dish) async throws {
    #expect(ingredient.isFresh)
    let result = try cook(ingredient)
    try #require(result.isDelicious)
    try #require(result == dish)
}
  • 지수적으로 조합이 늘어난다면, zip으로 1:1 페어링 가능: @Test(arguments: zip(...))

6. 테스트 구성을 위한 Suite와 Tag

Test Suite: 논리적 그룹화

  • @Suite 어노테이션으로 여러 테스트 함수/모음을 구조화
  • 중첩 구조 사용 가능
@Suite("Various desserts") struct DessertTests {
    @Suite struct WarmDesserts { ... }
    @Suite struct ColdDesserts { ... }
}

Tag: 다양한 기준별 종/횡 연결

  • 테스트끼리 구체적 타입·구조와 무관하게 태그로 묶을 수 있음
  • ex: 카페인 함유 음료, 초콜릿 포함 디저트 등
extension Tag {
    @Tag static var caffeinated: Self
    @Tag static var chocolatey: Self
}

@Suite(.tags(.caffeinated)) struct DrinkTests { ... }
@Test(.tags(.chocolatey)) func mochaIngredientProportion() { ... }
  • Xcode 테스트 내비게이터에서 태그 필터, 그룹핑 및 실행할 수 있음
  • 테스트 계획에 포함/제외 태그 지정도 지원

7. 테스트 병렬 실행과 직렬화 제어

  • Swift Testing은 기본적으로 모든 테스트를 병렬 실행
    • 빠른 실행, CI/CD 속도 향상, 숨겨진 종속성 오류 조기 발각

직렬 실행이 필요한 경우: .serialized

  • 데이터 종속성, Thread Unsafe 코드 등 반드시 순서 보장이 필요한 경우 모음에 .serialized 지정
@Suite("Cupcake tests", .serialized) struct CupcakeTests { ... }
  • 병렬 실행 가능한 영역에선 최대한 이를 활용하는 것이 권장

테스트 실행 순서 랜덤화

  • 병렬성 실질 효과 외에도 순서 의존성(잘못된 테스트 설계)을 잡아내는 효과도 있음

8. 비동기/동시성 테스트 지원 (async/await, Continuation, Confirmation)

async/await 사용 가능

@Test func bakeCookies() async throws {
    let cookies = await Cookie.bake(count: 10)
    try await eat(cookies, with: .milk)
}

콜백 기반 비동기 코드 연결

  • Swift 6에서 제공하는 async 오버로드 활용, 불가시엔 Continuation 패턴 활용
@Test func bakeCookies() async throws {
    let cookies = await Cookie.bake(count: 10)
    try await withCheckedThrowingContinuation { continuation in
        eat(cookies, with: .milk) { result, error in
            if let result { continuation.resume(returning: result) }
            else { continuation.resume(throwing: error) }
        }
    }
}

반복 콜백 카운트 검증(confirmation)

  • 콜백이 여러 번 발생하는 상황에선 confirmation("label", expectedCount:) 활용 가능
@Test func bakeCookies() async throws {
    let cookies = await Cookie.bake(count: 10)
    try await confirmation("Ate cookies", expectedCount: 10) { ateCookie in
        try await eat(cookies, with: .milk) { cookie, crumbs in
            #expect(!crumbs.in(.milk))
            ateCookie() // 콜백마다 호출
        }
    }
}
  • 예상 발생하지 않아야 할 경우엔 expectedCount: 0도 지정 가능

9. Xcode Cloud 및 테스트 인사이트 연동

  • Xcode Cloud상에서도 Swift Testing의 Suite, Tag, 결과 필터, 인사이트 기능 연동
  • 대규모 테스트 환경의 유지·개선·분석에 효율적으로 활용

profile
iOS 개발자 공부하는 Roy

0개의 댓글