apple/WWDC

Swift Testing으로 테스트 심화하기 (Go further with Swift Testing) - WWDC24

lgvv 2025. 6. 29. 23:51

Swift Testing으로 테스트 심화하기 (Go further with Swift Testing) - WWDC24

 
 

 
코드가 복잡해질수록 테스트를 읽고 이해하기 쉽도록 만드는 것이 더욱 중요
모든 엣지 케이스를 방어하기 위해 코드 커버리지를 적당히 유지
그룹으로 잘 관리하는것이 어려움.
테스트 사이에 숨겨진 종속성으로 인해 테스트가 취약해짐.
 

 
테스트는 일반적으로 완벽하기보단 덜 작성되는게 많지만, 잘못된 입력과 예상치 못한 조건을 마주친 경우에도 테스트가 깔끔하게 실패하게 해야함.

 
이렇게 코드를 작성한 경우에는 try 구문에서 테스트가 실패함.
만약 함수가 성공적으로 실행될 경우 expect에서 값을 확인할 수 있음.

 
반면에 테스트 실패 케이스가 의도대로 실패하는지 확인하려면 do - catch 문을 확인할 수 있음.
try 메서드 주변에 do - catch 구문을 추가해 오류를 검사하는 방법이 있지만, 이는 올바른 방법이 아닐 수 있음.
코드가 장황해지고, 에러가 캐치되지 않은 경우에 해당 코드로 올바르게 진행되었는지도 판단하기 어려움.
 

 
이 지점에서 Swift Testing의 expect throw가 도움을 줌.
do - catch 문을 직접 작성하는 대신 throws에 에러를 명시함.
이 경우에는 brew 메서드가 에러를 발생시킬 경우에 통과함.
오류가 없는 경우에는 테스트가 실패함.

 
특정 유형의 오류가 발생했는지 확인하라면 명시적으로 특정 에러의 타입을 줄 수 있음.
이 경우에는 에러가 발생하지 않거나 특정 오류의 인스턴스가 아니라면 실패함.
 
 

 
한단계 더 나아가서는 특정 오류가 발생했는지 더 엄격하게 검증할 수도 있음.
 
 

 
또한 expect throws 매크로의 유효성 검사를 커스터마이징 할 수도 있음.
에러의 유효성 검사를 커스텀해 특정 오류해 대해서 더 세밀하게 분석할 수 있음.
 
 

 
require을 사용할 경우 해당 에러에 대해서도 필수 값을 정의할 수 있음.
 

 
옵셔널의 유효성을 검사하는 경우에는 guard let 을 사용하기 보다 #require을 활용해 제어 흐름을 더 간결하게 가져갈 수 있음.
 
 

 
테스트에서 알려진 오류에 대해서 문서화하기 위해 withKnownIssue 함수를 사용할 수 있음.
테스트 실패를 즉시 수정할 수 없거나, 통제할 수 없는 요인으로 테스트가 실패하는 경우에 테스트 결과에 노이즈가 추가되어서 실제 사용자에게 발생하는 문제를 캐치하지 못할 수도 있음.
 
disabled 보다는 withKnownIssue를 사용하는 것이 더 나은 경우가 있는데 이 경우에는 테스트는 계속 실행되고 컴파일 오류에 대한 알림을 지속적으로 받을 수 있음.
 
함수가 오류를 반환하는 경우 예상했던 오류이기 때문에 테스트 결과가 테스트 실패에 포함되지 않음.
대신 결과에서 예상했던 실패로 나타남.
만약 문제가 해결되어서 테스트가 성공할 경우에는 다른 알림이 나타나서 문제 해결을 알려주고 그 경우에 withKnownIssue 호출을 제거하고 테스트를 다시 정상적으로 실행할 수 있음.
 

 
테스트 내에서도 여러 케이스에 대해서 #expect를 수행할 수 있는데, 이 경우 특정 expect에만 withKnownIssue로 래핑하고 나머지 유효성 검사가 진행되도록 할 수 있음.
 

 
커스텀한 테스트에서 문제를 안내하고 문제가 발생했을 때 설명을 추가할 수 있음.
인자가 복잡한 경우에는 테스트에 쓸모 없는 추가 데이터가 다수 포함될 수 있음.
 

 
테스트 정보는 정확하지만 포커싱 하는 부분을 캐치할 수 없어서 불편함.
이 경우에는 CustomTestStringConvertible 프로토콜을 통해 테스트 설명에 관련한 타입을 별도로 만들 수 있음.
이 경우에 더 가독성 높고 설명이 정확한 것을 확인할 수 있음.
 

 
코드 품질에 있어서 어려운건 모든 엣지 케이스를 다루는 것임.
엣지 케이스를 포함하기 전에 다양한 영역에서 테스트를 하는 것이 좋지만, 시간이 오래 걸리고 유지보수가 끔찍함.

parameterized test를 활용해 개선할 수 있음.
Swift Testing에서는 각 인자를 별개의 테스트 케이스로 분할함.
 

축: Time

 
이러한 테스트들은 서로 완전히 독립적이며, 병렬로 실행할 수 있음.
따라서 모든 테스트 케이스를 테스트하는데 필요한 시간이 크게 줄어듦.
 

 
매개변수화 된 테스트는 독립적으로 재실행하는 것을 지원함.
이를 통해 다른 테스트를 재실행하지 않고도 실패한 테스트만 재시도 할 수 있음.
 

 
매개변수화 된 테스트는 여러 타입을 인자로 받을 수 있음.
 

 
더 많은 argument를 전달하고자 할 경우에는 첫번째 argument 뒤에 다른 argument를 추가할 수 있음.
첫번째 요소는 arguments의 첫번째 요소로 전달되고, 다음 요소는 그에 맞게끔 매핑되어서 전달됨.
 
하나의 함수에서 모든 조합이 테스트 가능해졌으므로, 테스트 커버리지를 개선하는 강력한 방법임.
 


특정 값들을 순서대로 쌍으로 전달해야할 경우에는 인수에 zip 과 같은 오퍼레이터를 사용할 수 있음.
위 코드의 경우에는 순차적으로 zip의 형태로 4번 테스트를 수행함.
 
손쉽게 테스트를 확장할 수 있음.
 

 
Suite는 테스트 기능을 포함하는 타입
표시를 남기는 방식으로 이러한 기능을 문서화할 수 있음.
 

 
Swift Testing의 Suite는 다른 Suite를 포함할 수 있어서, 테스트를 더욱 유연하게 구성할 수 있음.
테스트 그룹간의 관계를 더욱 명확하게 할 수 있음.
 

 
직접 연결되어 있지 않아도 일부 테스트의 공통된 특성을 공유할 수 있음.
Tag를 활용해 서로 다른 Suite에서 테스트를 연결할 수도 있음.
하지만 주의할 점은 Tag는 테스트 Suite를 대체할 수 없음.
Suite는 소스코드 수준에서 테스트 함수에 구조를 부여하지만, Tag는 서로 다른 File, Suite, Target의 테스트를 연결할 수 있도록 도움
 
 
Tag를 선언하고 사용하기 위해서는 extension Tag를 활용함.
테스트는 해당 Suite에서 Tag를 상속함.
개별 메서드에 추가한 경우에는 개별 메서드만 공통적으로 묶임.
 
 

 
태그는 여러개를 추가할 수도 있음.
 
태그는 테스트 네비게이터가 그룹화함.
 
 

 
태그를 테스트에서 제외하고 싶은 경우에는 Filter를 사용해서 제외할 수 있음.
 

 
인사이트를 빠르게 확인 가능하고, Xcode Cloud에도 Swift Testing을 지원함.
 

 
Swift Testing은 병렬 테스트가 기본적으로 활성화 되어 있음.
모든 물리 디바이스에서 병렬 실행이 가능해짐
 

축: Time

 
XCTest에서는 모든 테스트가 기본적으로 직렬로 실행됨.
 

축: Time

 
Swift Testing에서는 기본적으로  병렬로 실행하는데
실행 시간이 단축되며, 1분 1초가 중요한 CI에서 더 빠른 결과를 얻을 수 있음.
동기식인지 비동기식인지 관계가 없음! 병렬로 실행된다는 것임!
 
XCTest와의 가장 큰 차이점으로 여러 프로세스를 사용하는 병렬화만 지원하여 한번에 하나의 테스트만 실행하는 것과는 다름.
필요하다면 테스트 함수를 MainActor와 같은 글로벌 액터에 격리할 수도 있음.

 
테스트의 실행 순서는 무작위로 지정됨.
이렇게 할 경우 테스트 사이에  숨겨진 종속성 드러내고 컨트롤이 필요한 영역을 노출할 수 있음.
 

 
XCTest에서는 직렬로 실행되기 때문에 문제가 없지만, Swift Testing으로 변경한 경우에 병렬 처리로 인해 cupcake의 유무에 테스트의 성공 실패가 영향을 받음.
 
이때는 .serialized 특성을 활용할 수 있음.
이를 명시하면 테스트를 직렬로 실행해야 함을 나타낼 수 있음.
하지만 serialize는 병렬 실행의
이점이 없어지게 됨.
 
가능하다면 테스트 코드를 병렬로 실행 가능하도록 리팩토링 하는 것을 고려해야 함.
 

 
또한 .serialized는 매개변수화 된 테스트 함수에 적용되어서 테스트 케이스가 한번에 하나씩 실행되도록 할 수 있음.
nested 형태의 @Suite를 포함하는 @Suite에도 자동 상속되므로 직렬 플래그를 2번 추가할 필요가 없음.
 

축: Time

 
Swift Testing은 .serialized가 있는 코드를 직렬로 실행함.
하지만 해당 플래그가 없다면 병렬로 처리해서 병렬로 처리하는 성능의 이점을 여전히 활용 가능함.
 
 
필요하다면 직렬로 실행할 수 있지만 병렬로 실행할 수 있게 리팩토링 하는 것이 좋음.
Swift Tesing은 기본적으로 병렬 테스트가 켜져 있어서 테스트를 가능한 빨리 실행할 수 있음.
또한 Swift 6가 병렬로 실행되지 못하게 하는 문제를 찾게 도와줌.

 
Swift Tesing을 활용해 비동기 조건에서 대기하는 방법에 대해서 알아볼 것.
 

 
동시적으로 실행되는 테스트 코드 작성시 Swift에서도 프로덕션 코드에서와 같이 동일한 동시성 기능을 사용할 수 있음.
await을 만나는 동안에 다른 테스트 코드가 CPU를 계속 사용할 수 있도록 테스트를 일시 중단함.
 

 
 
특히 C난 Objective-C 코드의 경우에는 컴플리션을 통해 코드가 완료 되었음을 알려줌.
하지만 컴플리션을 함수가 성공했는지 확인할 수 없음.
 
 

 
Swift는 대부분의 컴플리션 핸들러에 대해 대신 사용할 수 있는 비동기 오버로드를 자동으로 제공함.
 

 
만약 이를 사용할 수 없다면 withCheckedThrowingContinuation이나 withCheckedContinuation 해당 키워드를 통해 대기함.
 

 
또 다른 콜백은 두번 이상 실행될 수 있는 이벤트 핸들러임.
해당 버전의 eat 메서드는 전체 식사가 끝날 때마다 콜백을 호출하는 것이 아니라 각 쿠키마다 한번씩 호출함.
 


하지만 변수를 사용해 먹은 쿠키의 수를 계산하려고 하면, Swift 6에서 동시성 문제가 발생함.
왜냐하면 클로저에 캡처없이 변수를 세팅하는 것이 안전하지 않기 때문에.
 
만약 호출 횟수를 확인해야 하는 경우에는 confirmation을 대신 사용할 수 있음.
* 이건 테스트코드에서 callCount 프로퍼티로 체크하던건데 이걸로 간소화 가능할 것 같음.
 
confirmation은 기본적으로 1회만 발생할 것으로 예상하지만 다른 횟수를 지정할 수도 있음.
만약 confirmation이 아예 발생하지 않아야 한다면, 0을 지정할 수도 있음.
 


(참고)
 
https://www.youtube.com/watch?v=bOvWGHi-BxI&t=1441s