티스토리 뷰

해당 글은 WWDC24 Meet Swift Testing을 토대로 작성했습니다. 

Swift Testing이란?

Swift Testing은 Swift로 작성된 코드를 테스트하기 위한 새로운 오픈 소스 패키지입니다. 기존 XCTest에 비해 테스트를 더 쉽게 작성하고 관리할 수 있도록 도와줍니다. (Xcode16부터 사용 가능하며, 템플릿 default 설정은 XCTest가 아닌 Swift Testing이 됩니다.)


Swift Testing의 구성 요소

테스트 함수

  • Swift Testing에서는 기존 테스트 함수가 "test"로 시작했던 것과 다르게 @Test 속성을 사용해 테스트 함수임을 명시적으로 표현합니다.
  • 전역 함수, 정적 함수, 인스턴스 메서드 등 다양한 형태의 함수를 지원합니다.

Expectations

  • 테스트 통과 & 실패 유무 확인은 기존 XCTAssert api를 사용하는 것이 아닌, #expect와 #require 두 가지 매크로를 사용합니다.
  • 일반적인 표현식과 연산자를 사용하여 조건을 검증하기 때문에 테스트 작성이 직관적입니다.
  • #require 매크로의 경우 실패 시 테스트를 중단시킵니다.

Traits

  • 테스트별 또는 스위트별로 특성을 지정할 수 있습니다.
  • 조건부 실행, 테스트 비활성화, 버그 참조 등 다양한 trait을 제공합니다.

Suites

  • 테스트를 포함하는 타입으로 테스트 함수나 다른 스위트를 구조화한 것을 Suites라 합니다. (즉, 테스트 케이스 그룹화)
  • 구조체, 액터, 클래스등을 스위트로 사용 가능합니다.
  • @Suite 속성을 사용해 스위트임을 명시적으로 표현할 수 있습니다.
  • 프로퍼티를 가질 수 있고, 테스트 전후 로직 수행을 위해 init, deinit 사용이 가능합니다.

Swift Testing의 장점

  • 병렬 실행: Swift 동시성을 사용하여 테스트 병렬 실행이 가능하고 그에 따라 더 빠른 테스트 결과 도출이 가능합니다.
  • 매개변수화된 테스트: 여러 인수로 테스트 반복 실행이 가능합니다.

 

사용 예 (Building blocks)

아래 코드들은 WWDC24 Meet Swift Testing 영상에서 예시로 나오는 코드들로, DestinationVideo 모듈에서 비디오 파일의 메타데이터가 기대하는 메타데이터와 동일한지 테스트 하는 상황을 가정합니다.

import Testing
@testable import DestinationVideo

// @Test 속성을 사용해 테스트 함수임을 명시적으로 표현
@Test func videoMetadata() {
    let video = Video(fileName: "By the Lake.mov")
    let expectedMetadata = Metadata(duration: .seconds(90))
    // 테스트 결과(기댓값) 확인 
    #expect(video.metadata == expectedMetadata)
}

테스트 실행 결과 fail시, 왜 fail 인지 자세하게 확인 가능 (Show 버튼)
Show 버튼 눌렀을 때

테스트 실행 결과, 위와 같이 테스트가 실패 하고 (metadata > duration, resolution이 다름), 테스트 실패 원인을 파악해 보니 Video 타입이 초기화시 metadata 초기화를 누락한 것을 알게 됩니다.

위와 같이 initializer를 수정 후 다시 테스트 실행하면? 아래와 같이 성공하게 됩니다.

위에 설명했던 것처럼, #expect 매크로는 매우 유연하고 직관적입니다. (XCTest에서 XCTAssertEqual, XCTAssertLessThan 같이 다양한 Assertion api 들을 이해하고 사용할 필요 없이 일반적인 표현식과 연산자를 사용하면 되기 때문)

테스트 실행 도중 실패시 멈추고 싶을 땐 #expect 매크로가 아닌 try #require() 매크로를 사용하면 됩니다. 그럼 테스트 실패시, 테스트를 더 진행하지 않습니다.

try #require(session.isValid) // 테스트 실패시, error throw & 테스트 더 진행하지 않음

// 해당 라인은 첫번째 라인이 성공했을 때만 의미가 있기 때문에 테스트 실패시 테스트를 멈추도록 try #require 사용한 케이스
sesiion.invalidate()

이 경우도 마찬가지로, paymentMethods.first가 nil이고, 더 테스트를 진행할 필요가 없기 때문에 테스트는 fail되고 중단됨

테스트의 경우 아래와 같이 custom display name을 사용할 수 있습니다. (아래 코드에서 "Check video metadata")

@Test("Check video metadata") func videoMetadata() {
    let video = Video(fileName: "By the Lake.mov")
    let expectedMetadata = Metadata(duration: .seconds(90))
    #expect(video.metadata == expectedMetadata)
}

그럼 테스트 목적을 조금 더 구체적으로 설명할 수 있고, 아래 이미지처럼 테스트 네비게이터에도 직접 지정한 custom display name으로 뜨게 됩니다.

Test 함수와 Expectations에 대해 알아봤으므로, 그 다음으로는 Traits를 알아보도록 합시다.

Testing에서 제공하는 Built-in traits는 아래와 같습니다.

  • @Test("..."): 테스트 디스플레이 네임 지정
  • @Test(.bug("...", "..."): 버그 주석과 함께, 관련 문제 참조 URL을 전달, XCode16 테스트 보고서에서 해당 버그 특성을 보고 URL을 클릭해 열어볼 수 있음
  • @Test(.tags(...)): 테스트 태그 지정
  • @Test(.enabled(if: ...)): 테스트 실행 전 평가할 조건을 전달하고, 실패시 테스트는 건너뜀
  • @Test(.disabled("...")): 테스트 비활성화 시 사용, 문자열은 왜 해당 테스트를 비활성화 하는지 설명 기입, 실행하지 않을 테스트를 주석치는 것과의 차이점은 테스트 코드가 정상적으로 컴파일 되는지 확인 가능 (실행만 안 할 뿐)
  • @Test(...) @available(macOS 15, *): 테스트 본문 전체가 특정 OS 버전에서만 실행될 수 있는 경우
  • @Test(.timeLimit(...): 테스트 함수 maximum timeLimit 설정

(해당 traits의 사용 예시는 아래 Common workflows에서 확인 가능합니다.)


테스트가 여러개일 때는 아래와 같이 Suites로 구조화 할 수 있습니다.

import Testing
@testable import DestinationVideo

// 테스트 스위트 (테스트 그룹화)
struct VideoTests {
    let video = Video(fileName: "By the Lake.mov")
    
    @Test("Check video metadata") func videoMetadata() {
        let expectedMetadata = Metadata(duration: .seconds(90))
        #expect(video.metadata == expectedMetadata)
    }
    
    @Test func rating() async throws {
        #expect(video.contentRating == "G")
    }
}

그럼 아래와 같이 계층 구조가 테스트 네비게이터에 반영되고, 테스트를 그룹으로 실행 가능합니다. @Test 함수나, @Suites를 포함하는 모든 타입은 암묵적으로 @Suite로 간주되기 때문에 @Suite를 명시하지 않아도 상관 없습니다. (추가로 아래 이미지 속 코드에는 나오지 않지만 set-up & tear-down 로직이 있을 경우 init, deinit에서 수행하면 됩니다.)

 

Common workflows

위에 설명한 Built-in traits 사용

1. 조건부 테스트

.enabled 조건 실패시 해당 테스트 수행 스킵
.disabled와 .bug trait을 사용한 케이스, 왜 테스트를 수행 하지 않는지, 버그와 관련된 참조 링크 표시
해당 테스트가 특정 OS와 버전에 의존성이 있는 경우

2. 공통 특성을 갖는 테스트

테스트 네비게이터에서는 아래와 같이 태그로 테스트를 구분하여 확인 및 실행이 가능하고 테스트 보고서에서 tag별 필터링이 가능합니다.

3. 반복 테스트 (Tests with different arguments)

서로 다른 비디오에 대해 같은 테스트를 수행한다고 가정했을 때, 아래와 같이 테스트를 구성하면 매우 비효율적이게 되는데, 이럴 경우 arguments를 사용해, 비디오이름을 인자로 받는 단일 테스트케이스를 작성할 수 있습니다.

아래 코드는 arguments 사용해 videoName을 인자로 받는 단일 테스트 케이스입니다.

struct VideoContinentsTests {
    @Test("Number of mentioned continents", arguments: [
    	"A Beach",
        "By the Lake",
        "Camping in the Woods",
        "The Rolling Hills",
        "Ocean Breeze",
        "Scotland Coast",
        "China Paddy Field"
    ])
    func mentionedContinentCounts(videoName: String) async throws {
    	let videoLibrary = try await VideoLibrary()
        let video = try #require(await videoLibrary.video(named: videoName))
        #expect(!video.mentionedContinents.isEmpty)
        #expect(video.mentionedContinents.count <= 3)
    }
}

위 테스트 수행시, 특정 인자에서 실패한다면, 아래와 같이 어떤 인자에서 실패했는지 확인이 가능합니다. 

위와 같은 테스트는 단일 테스트 본문 내에서 for in loop을 사용하는 것과 유사해 보이지만 arguments를 사용한 테스트의 경우 for in loop를 사용한 테스트보다 몇가지 장점이 있습니다. 

arguments를 사용한 테스트의 경우 개별 인수 세부 정보를 테스트 결과에서 확인할 수 있으며 디버깅을 위해 특정 인수만 독립적으로 실행 이 가능 합니다. 그리고, 테스팅을 병렬적으로 실행하기 때문에 테스트 실행 결과 역시 빠르게 확인해 볼 수 있다는 장점을 갖고 있습니다.

 

XCTest와의 차이

Swift Testing의 구성 요소와 특징들을 기존 XCTest와 비교해 보면 여러모로 정말 좋아졌다는 걸 느낄 수 있습다. 

댓글
링크
최근에 올라온 글
최근에 달린 댓글