• 코드 테스팅을 위한 새로운 오픈소스 패키지
    • 테스트를 서술적이고 구조적이게 표현 가능
    • 테스트 실패시 수행가능한 세부 사항을 제공
    • 큰 코드 베이스에서도 확장 가능
  • Swift를 위해서 디자인 되었고, 동시성과 매크로 등 모던 기능들을 포용한다.
  • 모든 주요 플랫폼(Linux, Windows)을 지원
  • 오픈소스로 개발되어 커뮤니티에서 발전에 참여 가능
  • Building Blocks
    • 테스트를 작성해본 적이 없다면, 테스트 타겟 추가부터 해야 한다.

      • File > New > Target → Unit Testing Bundle
      • Xcode 16 부터는 Swift Testing이 기본 옵션
    • 테스트 작성

      • @Test 애트리뷰트가 있으면 테스트로 인식하고 Xcode가 run 버튼을 띄워준다.
      • 전역 함수와 메소드 모두 가능
      • async, throws, global actor isolation 모두 가능
      import Testing
      
      @Test func videoMetadata() {
          // ...
      }
      
    • 단정문 작성

      • 원하는 조건이 참인지 확인한다.

      • 일반적인 표현식과 연산을 받는다.

      • 실패 했을 때 소스코드와 서브 표현식의 값을 캡쳐한다.

        스크린샷 2024-06-19 오전 10.18.20.png

      import Testing
      @testable import DestinationVideo
      
      @Test func videoMetadata() {
          let video = Video(fileName: "By the Lake.mov")
          let expectedMetadata = Metadata(duration: .seconds(90))
          #expect(video.metadata == expectedMetadata)
      }
      
    • expect 매크로 예시

      • 에러에서 값도 캡쳐해서 보여준다.
      • 특별한 API를 사용할 필요 없이 하나로 통일되었다.

      스크린샷 2024-06-19 오전 10.20.38.png

      #expect(1 == 2)
      
      #expect(user.name == "Alice")
      
      #expect(!array.isEmpty)
      
      #expect(numbers.contains(4))
      
    • 조건이 맞지 않으면 테스트를 빠르게 끝내고 싶을 때는 #require 매크로를 사용하라.

      • 에러를 던진다.

        try #require(session.isValid)
        
        session.invalidate()
        
      • 언래핑에도 쓸 수 있다.

        let method = try #require(paymentMethods.first)
        
        #expect(method.isDefault)
        
    • 테스트에 커스텀 이름 붙이기

      • Test Navigator와 Xcode의 다른 곳에서 보여진다.

      • Trait의 예시

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

      • 테스트에 대한 서술 정보 표현

      • 테스트 실행 시점, 실행 여부 커스텀

      • 테스트 동작 수정

        스크린샷 2024-06-19 오전 10.26.44.png

    • 테스트 그룹화

      • struct로 감싸면 된다. 이 타입이 Test Suite가 된다.

      • @Suite 매크로로 명시적으로 표현하는 것도 가능하다.

        struct VideoTests {
        
            @Test("Check video metadata") func videoMetadata() {
                let video = Video(fileName: "By the Lake.mov")
                let expectedMetadata = Metadata(duration: .seconds(90))
                #expect(video.metadata == expectedMetadata)
            }
        
            @Test func rating() async throws {
                let video = Video(fileName: "By the Lake.mov")
                #expect(video.contentRating == "G")
            }
        
        }
        
      • 상태를 가지는 것도 가능하다.

        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")
            }
        
        }
        
      • init과 deinit으로 setup과 teardown 로직을 수행한다.

      • @Test 메소드마다 한개씩 초기화되어서 원하지 않는 상태 공유를 방지한다.

    • 이 모든 것이 Swift를 위해서 디자인 되었다.

      • 테스트 함수는 동시성과 actor isolation을 지원함으로 매끄럽게 통합된다.
      • expect에서도 async/await을 쓸 수 있고 모든 내장 언어 연산을 지원하다.
      • expect과 trait모두 Swift macro를 기반으로 해서 실패 메시지를 상세하게 보여주고 테스트 별 정보를 코드에서 바로 볼 수 있다.
      • suite는 값 시멘틱을 사용해서 상태 격리에 구조체를 권장한다.
  • Common workflows
    • Test With Conditions

      • 런타임에 평가 되어야 하는 조건을 명시해서 false면 테스트를 실행하지 않는다.

        @Test(.enabled(if: AppFeatures.isCommentingEnabled))
        func videoCommenting() {
            // ...
        }
        
      • 아예 안 돌리려면 .disable trait을 사용한다.

        • 컴파일 되는 것은 테스트 할 수 있기 때문에 주석처리하는 것보다 좋다.
        • 메시지도 테스트 결과에 항상 남기 때문에 CI에서 볼 수 있다.
        @Test(.disabled("Due to a known crash"))
        func example() {
            // ...
        }
        
      • 버그 추적 시스템과 연결짓기

        @Test(.disabled("Due to a known crash"),
              .bug("example.org/bugs/1234", "Program crashes at <symbol>"))
        func example() {
            // ...
        }
        
      • 특정 버전에서만 테스트가 돌아야 하는 경우는 함수 자체에 @available을 붙여야 리포트에서 이를 반영해줄 수 있다.

        // ❌ Avoid checking availability at runtime using #available
        @Test func hasRuntimeVersionCheck() {
            guard #available(macOS 15, *) else { return }
        
            // ...
        }
        
        // ✅ Prefer @available attribute on test function
        @Test
        @available(macOS 15, *)
        func usesNewAPIs() {
            // ...
        } 
        
    • Tests with common characteristics

      • 태그를 통해서 다른 Test Suite나 파일에 있더라도 연관지을 수 있다.

        @Test(.tags(.formatting)) func rating() async throws {
            #expect(video.contentRating == "G")
        }
        
      • suite 단위로도 붙일 수 잇고, 이 경우는 해당 suite아래의 함수에 모두 같은 태그가 상속된다.

        @Suite(.tags(.formatting))
        struct MetadataPresentation {
            let video = Video(fileName: "By the Lake.mov")
        
            @Test func rating() async throws {
                #expect(video.contentRating == "G")
            }
        
            @Test func formattedDuration() async throws {
                let videoLibrary = try await VideoLibrary()
                let video = try #require(await videoLibrary.video(named: "By the Lake"))
                #expect(video.formattedDuration == "0m 19s")
            }
        }
        
      • 특정 태그가 달린 테스트만 실행하거나, 테스트 리포트에서 특정 태그만 필터링할 수 있다.

      • 테스트 포함/제거 여부를 결정할 때 테스트 이름으로 필터링하는 것보다 태그를 권장한다.

      • 최적의 결과를 위해서는 최적의 trait을 사용할 것

        • 런타임 조건을 위해서는 .enabled(if:)를 사용한다던지
        • 심화적인 내용은 Go Further with Swift Testing 세션 참조
    • Tests with different arguments

      • 같은 테스트 내용인데 인자만 다를 때 함수를 여러번 만들면 코드 중복 및 이름짓기의 어려움 등의 문제가 있다.
      • 그래서 Swift-Testing을 Parameterized testing을 지원한다.
        • 인자를 추가해야한다. 그러면 인자를 추가해야 한다고 에러가 난다.

        • 인자를 Test 매크로에 추가하고, 매개변수를 사용하도록 테스트를 수정한다.

        • 매개변수를 쓰더라도 여전히 이름이나 여러 trait들을 적용하는데는 문제가 없다. 매개변수 앞에다 써주면 된다.

          struct VideoContinentsTests {
          
              @Test("Number of mentioned continents", arguments: [
                  "A Beach",
                  "By the Lake",
                  "Camping in the Woods",
                  "The Rolling Hills",
                  "Ocean Breeze",
                  "Patagonia Lake",
                  "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)
              }
          
          }
          
        • 특정 매개변수만 따로 돌리는 것도 Test Navigator를 통해서 가능하다.

      • for-in 루프를 통해서 할 수도 있지만 권장하지 않는다.
        • 결과에서 개별 인자값이 명확하게 드러나지 않는다.
        • 개별적으로 돌려가면서 디버깅할 수도 없다.
        • 각 인자를 병렬로 돌릴수도 없다.
        • Parameterized test는 input이 여러 개일 때 이를 조합하여 돌리는 것도 제공한다.
  • Swift Testing and XCTest
  • Open Source