Swift

Go further with Swift Testing (feat. WWDC 2024)

GREEN.1229 2024. 9. 30. 17:14

안녕하세요. 그린입니다 🍏

이번 포스팅에서는 저번 Swift Testing에 대해 어떤건지 톺아봤다면 이번엔 좀 더 자세히 알아보려합니다.

심화편이라고 생각하면 좋아요 😁

WWDC 2024의 Go further with Swift Testing 세션으로 한번 살펴보겠습니다 🙋🏻

 

먼저 Swift Testing이 어떤건지 기초부터 알고 싶다면 아래 포스팅을 보고 오세요 😁
 

Meet Swift Testing (feat. WWDC 2024)

안녕하세요. 그린입니다 🍏이번 포스팅에서는 WWDC 2024에서 소개된 Meet Swift Testing 세션을 보면서 Swift Testing에 대해 알아보려합니다 🙋🏻  Meet Swift Testing우리는 그전까지 테스트하면 XCTest를 사

green1229.tistory.com

 


Go further with Swift Testing


Expressive code

읽기 편한 즉, 가독성이 좋은 테스트 코드가 필요합니다.

Expectations

먼저 Swift Testing은 Swift 언어 기능과 표현을 활용해 표현력이 뛰어나고 간결한 인터페이스를 제공해줍니다.

 

#expect(person.hadCoffe)
#expect(teaTypes.count > 10)
#expect(iceCreamToppings != nil)

 

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)
}

 

이렇게 thorws를 통해 에러 테스트도 충분히 테스트가 가능합니다.

만약 에러가 try에서 에러가 난다면 친절히 알려주죠.

 

 

만약 try에서 에러로 걸리지 않고 정상적이면 expect 매크로를 통해 확인할 수 있죠.

그냥 우리가 편하게 함수 구현하듯이 처리하면 되죠!

 

더 나아가서 Expect throws 매크로를 통해 do catch 구문을 작성해줄 수 있습니다.

 

import Testing

@Test func brewTeaError() throws() {
  let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4)
  #expect(throws: (any Error).self) {
    try teaLeaves.brew(forMinutes: 200)
  }
}

 

물론, do catch 구문을 통해 오류 체크를 할 수도 있지만 장황하게 그렇게 쓸 필요가 전혀 없는것이죠.

해당 코드에서는 brew try 함수에서 오류 발생 시 테스트는 통과하게되고, 오류가 발생하면 테스트가 실패됩니다.

조금 혼란스럽긴 한데, 그게 Expect Throws 매크로입니다.

 

#expect(throws: BrewingError.self)

 

이렇게 특정 타입의 오류 발생을 확인할 수도 있습니다.

오류가 발생하지 않거나 BrewingError의 인스턴스가 아닌 오류가 발생하면 테스트는 실패하게 됩니다.

 

#expect(throws: BrewingError.oversteeped)

 

더 나아가서 이렇게 특정 오류 발생 여부를 확인할 수도 있죠.

 

만약 조건이 더 복잡한 경우라면 해당 매크로 수행에 대해 커스텀화 할 수 있습니다.

 

 

Required expectations 

expect 매크로가 아닌 require 매크로로 꼭 필수 기대값으로 변경할 수 있어요.

 

 

이렇게 해당 color라는 프로퍼티가 꼭 필수적인지를 사용할 수 있어요.

그럼 expect로 테스트를 하지 않고 color 프로퍼티가 없다면 거기서 테스트를 조기 종료 시킵니다.

 

Known issues

이제 현재는 고칠 수 없지만 알려진 이슈로 추후 수정될 기능에 대해 테스트를 배제하거나 기록해놓는 방법에 대해 알아봅니다.

 

 

makeSoftServe 함수에 무슨 문제가 있어 현재 수정중이면 해당 테스트에 실패하게 됩니다.

그런데 해당 함수 수정에 시간이 걸릴 수 있어 테스트를 넘어가려 합니다.

 

 

그럴때 이렇게 테스트를 disabled 처리해 아예 테스트를 하지 않을 수도 있지만, 이건 좋은 방법이 아니죠.

아예 테스트를 하지 않고 기록도 하지 않는것이니까요.

 

그래서 이럴 경우에는 withKnownIssue 기능을 사용하는것이 좋습니다.

 

 

테스트는 계속되며 컴파일 오류에 대한 알림을 받게 됩니다.

 

 

함수가 오류를 반환하는 경우 테스트 결과는 이미 예상된 실패이기에 테스트 실패로 간주되지 않아요.

대신 테스트는 결과에 예상된 실패로 표시가 됩니다.

추후 문제가 해결되면 해당 withKnownIssue 구문 호출을 지우고 테스트를 정상적으로 다시 실행하면 됩니다.

 

테스트에선 여러 검사도 수행할 수 있어요.

 

 

이렇게 실패한 함수를 withKnownIssus로 래핑하고 나머지를 유효성 검사가 실행되도록 허용할 수 있죠.

 

Custom test descriptions

커스텀 테스트를 할 수 있는 방법에 대한 소개입니다.

 

 

이렇게 테스트할 데이터가 많다면 테스트 시 해당 정보들이 Xcode에 다 표시되는데요.

 

 

이렇게 보기 복잡하게 정보들이 나타나게 됩니다.

물론, 정보 자체는 정확하지만 구분하기가 한눈에 쉽진 않죠.

 

이럴 경우에 코드에 영향을 주지 않고 각 항목에 간결한 테스트 설명을 제공할 수 있습니다.

 

 

타입에 CustomTestStringConvertible 프로토콜을 채택하고 적절히 원하는 설명이 되도록 testDescription을 구현하면 됩니다.

 

 

훨씬 더 간결한 커스텀한 테스트 설명을 볼 수 있어요!

 


Parameterized testing

테스트에서 중요한것 중 하나는 거의 발생하지 않는 복잡한 기능과 같은 모든 극단적인 케이스들을 잘 다룰줄 알아야합니다.

물론, 극단적인 케이스를 위해 다양한 조건에서 테스트를 하는것도 괜찮지만 유지관리에도 힘들고 공수가 많이 듭니다.

Swift Testing으로 다양한 인수를 사용하면 단일 테스트 함수를 쉽게 실행할 수 있어요.

 

 

이렇게 테스트를 하면 보일러 플레이트 코드가 너무 많아지죠.

실제론 이걸 통합해서 하나의 테스트로 합칠 수 있어요.

 

 

이렇게 매개변수화된 테스트를 사용할 수 있습니다.

하나 이상의 매개변수를 사용하면서 동일한 로직의 테스트를 돌릴때 유용하죠.

테스트 함수가 실행되면 Swift Testing은 이것들을 자동으로 인수당 하나씩 별도의 테스트 케이스로 분할해요.

 

 

테스트들은 그럼 각각 완전히 독립적이로 병렬로 실행될 수 있습니다.

반복문을 사용하거나 별도의 테스트 함수로 만드는것보다 훨씬 더 적은 시간으로 끝낼 수 있습니다.

 

 

Xcode는 Codable을 준수하는 경우에 테스트 함수의 개별 테스트 케이스 재실행을 지원해줍니다.

이를 통해서 실패한 개별 테스트 케이스를 다시 시도할 수도 있어요.

 

 

매개변수화된 테스트를 위해선 이렇게 arguments로 담아주는것이 끝입니다.

단순한 변화로 더 편리하고 시간도 절약하는 테스트를 구현할 수 있습니다.

 

만약 한 케이스에서 테스트 실패했다면 에러 이유를 명확히 나타내줍니다.

 

 

여기서 매개변수화된 테스트 함수는 다른 많은 타입의 입력도 받을 수 있습니다.

Array, Set, OptionSet, Dictionary, Range 모든 컬렉션을 사용할 수 있죠.

 

또한, 여러 타입으로 복합적으로 넘겨줄 수도 있습니다.

 

 

첫번째 컬렉션의 모든 요소가 테스트 함수의 첫번째 인수로 두번째도 두번째로 전달됩니다.

그리고 조합은 자동으로 테스트됩니다.

즉 조합상 경우의 수가 커지고 더 극대화되는 테스트 효과를 제공할 수 있죠!

 

 

이런식으로 3~4개 컬렉션으로 모든 기하급수적인 조합을 쉽게 테스트할 수 있죠.

 

또한, Swift의 zip 기능을 사용할 수도 있어요.

 

 

쌍을 이루는 튜플을 생성하죠.

 


Organizing tests

Suite는 테스트 기능을 포함하는 유형이란걸 이전 포스팅을 통해 알고 있습니다.

 

 

이렇게 해당 기능을 통해 테스트 이름을 줄 수 있도록 문서화할 수 있죠.

Swift Testing에선 이제 Suite에 다른 Suite를 포함해 더 유연한 테스트 구성을 제공합니다.

 

 

이런식으로 하위로 그룹화를 시켜 더 명확히 만들 수 있어요.

 

또한 태그를 통해 테스트를 구성하는데 더 도움을 줄 수 있습니다.

 

 

이렇게 연관있는 테스트를 가지고 태그를 만들어 추가할 수 있습니다.

 

Xcode 16에서 태그는 이렇게 사용됩니다.

 

 

이렇게 원하는 태그를 검색해 모아서 필터시켜 볼 수 있어요.

 

전체 테스트 탐색기를 펼쳐서 모든 테스트와 달린 태그들을 확인할 수도 있습니다.

 

 

여기서 Exclude Tags나 Include Tags를 활용해 원하는 테스트만 테스트하거나 볼 수 있습니다.

이렇게 조정된 테스트 계획을 실행하면 아래와 같이 테스트 보고서가 산출됩니다.

 

 

여기서는 더 자세히 세부 항목들을 보면서 어떤것들이 자세히 실패했는지 변경 사항도 검토하고 테스트도 수정할 수 있죠.

 

Xcode Cloud도 Swift 테스트를 지원하도록 업데이트 되었습니다.

 

 

Xcode와 같이 App Store Connect의 Xcode Cloud 탭에서 테스트 제품군의 결과를 볼 수 있어요.

 

결국 태그를 사용함으로 대규모 테스트 셋을 더욱 효율적으로 탐색하고 관리할 수 있는 장점이 있습니다 👍

 

마지막으로 테스트를 병렬로 실행하는 방법에 대해 살펴보겠습니다!

 


Testing in parallel

규모가 크고 관리하기 쉬운 테스트 세트들이 있기에 병렬 테스트를 통해서 더 빠르게 테스트를 하고 동시 환경에서 안정적으로 실행되도록 고려해야 합니다.

 

Swift Testing에선 병렬 테스트가 기본적으로 활성화되어 있어 추가 코드 작업 없이 활용할 수 있어요.

 

 

또한 처음으로 모든 디바이스에서 병렬 테스트를 실행할 수 있는 장점도 탑재되었습니다.

 

XCTest에서는 직렬적으로 사용합니다.

 

 

이게 기본적으로 테스트를 실행하는 방법이죠.

 

 

이렇게 병렬로 실행된다면 실행 시간이 단축되고 CI에서 아주 효과적일겁니다.

 

Swift Testing은 각각 한번에 하나의 테스트를 실행해 여러 프로세스를 사용해 병렬화하기에 XCTest와는 가장 큰 차이점입니다.

 

테스트 기능은 필요할때 메인 액터와 같은 전역 액터로 격리될 수 있어요.

테스트는 무작위의 순서로 병렬화됩니다.

 

 

이렇게 만약 테스트가 병렬화로 실행되어 첫번째 테스트에 대한 두번째 테스트가 종속성을 갖게되면 런타임 에러가 날 수 있죠.

 

serialized 특성을 이용해 해결해볼 수 있습니다.

테스트를 순차적으로 실행함을 보장하게끔 하죠.

serialized를 매개변수화된 테스트 함수에 적용해 해당 테스트 케이스가 한번에 하나씩 실행될 수 있도록 합니다.

serialized를 통해 차례로 실행되는데 직렬화긴 하죠 그러면?

그런데 이런 직렬화된 테스트와 병렬로 관련되지 않은 다른 테스트를 여전히 자유롭게 실행할 수 있어요.

즉, 병렬적 성능을 얻을수 있는것이죠.

 

 

즉, 필요한 경우 직렬과 병렬을 적절히 사용하여 테스트를 해야 합니다.

Swift 6가 이런 병렬 실행을 방해하는 테스트 문제를 찾는데 도움을 줄 수 있어요.

 

다음으로 Swift Testing을 상요해 비동기 조건을 기다리는것을 보겠습니다.

 

 

이런 동시성 코드에서 await를 만나 작업이 보류되는 동안 다른 테스트 코드가 CPU를 계속 사용하도록 허용하여 테스트를 임시 중단합니다.

 

 

이렇게 C 혹은 옵젝씨로 작성된 이전 코드는 완료 핸들러를 사용해 비동기 작업의 끝을 알립니다.

다만 이 코드는 테스트 함수가 반환된 후 실행되며 함수가 성공했는지는 알 수 없어요.

 

그렇기에 대부분의 완료 핸들러에 대해 Swift는 대신 사용할 수 있는 비동기 오버로드를 자동으로 제공합니다.

 

 

withCheckedThrowingContinuation이나 withCheckedContinuation을 사용해 변환하는것도 가능하죠.

 

 

해당 코드는 Swift 6에서 동시성 오류가 발생해요.

즉, 이런 방식으로 변수를 설정하는것이 안전하기 않기 때문이죠.

 

 

그렇기에, 이런식으로 confirmation을 활용하는것이 적절합니다.

 


마무리

진짜 보면 볼수록 편리한데 또 딥하게는 어떻게 작성해볼지 아직 감이 안오지만 다들 한번 해보시죠

 


레퍼런스

 

Go further with Swift Testing - WWDC24 - Videos - Apple Developer

Learn how to write a sweet set of (test) suites using Swift Testing's baked-in features. Discover how to take the building blocks further...

developer.apple.com