티스토리 뷰
[iOS] UITest - XCUITest
이번 포스팅에서는 UITest에 대해서 다뤄볼 예정이다!사용자 인터렉션을 테스트하여 UI플로우를 검증하는 XCUITest와,실제 픽셀 단위로 UI를 제대로 구성했는지 확인해 볼 수 있는 Snapshot Test에 대해
yanni13.tistory.com
바로 전 포스팅에서 UITest에 대해서 알아봤었다.
이번 블로그에서는 또 다른 UITest인 Snapshot 테스트에 대해서 공부해 보자!
✏️ Snapshot test란?
특정 뷰를 기준으로 스냅샷을 저장하고 이후 실행 시 새로 생성하는 테스트 기법
snapshot 테스트의 가장 큰 장점은 의도치 않은 UI 변경을 빠르게 감지할 수 있다는 점이다. 코드 수정이 실제 화면에 영향을 주었는지 자동으로 확인할 수 있고 결과를 픽셀 단위로 비교하기 때문에 아주 작은 차이로도 테스트에 실패하게 된다.
그래서 디자인 QA를 할 때도 앱을 빌드해서 매번 확인해야 할 필요 없이 불필요한 비용을 감소시킬 수 있다. 원래 같으면 D 화면의 UI를 확인하고 싶을 경우 A→B→C→D를 거쳐서 매번 확인해야 했다. 이 경우 항상 B화면에서 로그인을 해야 하는 경우 시뮬레이터로 테스트 해보는 시간이 생각보다 길어질 수도 있다.
하지만 snapshot 테스트를 활용하면 테스트하고 싶은 D 화면의 Snapshot을 찍어 바로 비교하며 확인할 수 있고 그 이미지 파일을 전달하는 방식으로 QA를 진행할 수도 있다.
다만 snapshot 테스트는 1px의 오차로도 테스트가 실패하기 때문에 허용범위를 얼마나 주는지가 중요하다. 실제 현업에서도 픽셀단위의 UI 때문에 디자인팀이랑 많은 논의를 한다고 한다.
이러한 이유들로 snapshot 테스트 또한 결코 무시할만한 주제가 아니라고 생각해서 직접 프로젝트에 적용해 보고 실제 협업과정에서 핵심적인 역할을 하는지, 개발 과정에서 얼마나 효율성을 가져다주는지 테스트해보고 싶어서 공부해 보게 되었다!
작동원리
- 피그마로 그린 결과 이미지 or 완성된 시뮬레이터 스크린샷을 기준 스냅샷으로 설정한다.
- 같은 조건에서 뷰를 다시 렌더링 하여 기준 스냅샷이랑 코드로 작성된 뷰랑 UI를 비교한다.
Snapshot test 도구로써는, 뱅크샐러드에서 만든 AXSnapshot이 있고, Point-Free SnapshotTesting등 라이브러리가 되게 많이 있다.
(회사 규모에서 만든 라이브러리가 많다는 것은 내부에서도 snapshot 테스트를 많이 사용하고 있다는 것 아닐까..)
GitHub - uber/ios-snapshot-test-case: Snapshot view unit tests for iOS
Snapshot view unit tests for iOS. Contribute to uber/ios-snapshot-test-case development by creating an account on GitHub.
github.com
나는 그중에서도 uber에서 만든 iOS-snapshot-test-case를 프로젝트에 적용해 보기로 하였다.
라이브러리 README.md 파일에 보면 스냅샷을 소스 코드 저장소에 저장된 "참조 이미지"와 비교하여 두 이미지가 일치하지 않으면 테스트가 실패한다고 한다.
그럼 어떻게 사용하는지 한번 살펴보자!
사용방법
1. reference 이미지가 저장될 파일 경로를 추가

snapshot 테스트를 비교해 보기 위해선 레퍼런스 이미지가 저장될 경로를 지정해줘야 한다.
가이딩에는 레퍼런스 이미지가(= 디자인 다 된 이미지!) 저장될 경로랑, 실패 이미지가 저장될 경로까지 지정하라고 권장하고 있다.

EditScheme -> Run을 눌러주면 환경변수 설정 하는 곳에 레퍼런스 이미지가 저장될 곳 경로를 지정해 주어야 된다.
스냅샷 테스트를 위해선 레퍼런스 이미지가 저장될 경로는 필수로 지정해줘야 한다.
2. 레퍼런스 이미지를 생성
final class ScanMateSnapshotTests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
self.recordMode = true
}
}
일반적으로 테스트 코드를 작성할 때 XCTest를 import 하고 XCTestCase를 상속받아 테스트를 구현해야 하는데, 스냅샷 테스트 같은 경우는 외부 라이브러리를 사용하다 보니 자체적으로 만든 FBSnapshotTestCase를 상속받아야 하고 상속받은 클래스 내부에서 스냅샷 테스트 코드를 작성해야 한다.
스냅샷 테스트를 처음 작성할 때 레퍼런스 이미지가 필요한데, 이때 setUp() 함수 안에 recordMode를 true로 활성화시켜놓으면, 테스트 대상 뷰의 스냅샷이 실제 이미지 파일로 생성되며 스키마에서 설정한 폴더 경로 안에 저장된다.
보통 처음 한 번만 recordMode = true로 돌려서 레퍼런스 이미지를 만든 뒤에, 진짜 비교 검증을 하기 위해선 recordMode에 false 값을 주어 테스트를 수행해야 한다.
만약 UI가 변경될 경우 다시 recordMode를 true로 활성화시켜서 레퍼런스 이미지를 갱신시켜주어야 한다.
피그마로 레퍼런스 이미지를 설정하고 싶을 경우는?
아까 레퍼런스 이미지를 저장할 output 폴더를 스키마에서 생성해 줬었다.
그 경로에 실제 구현된 스크린샷 이미지가 저장되어 있을 텐데, 이걸 디자이너가 만들어준 피그마 이미지와 비교하고 싶을 경우에는 해당 폴더에 있는 이미지를 피그마에서 추출한 이미지 png로 교체해 주면 된다.
3. 테스트 코드 작성
📍주의할 점
: setUp 코드만 작성한다고 레퍼런스 이미지가 생성되는 것은 아니다!
실제 테스트 함수에서 어떤 뷰를 스냅샷 찍을지 지정해야 레퍼런스 이미지가 생기고, setUp 함수에 있는 recordMode를 활성화 시켜줘야 테스트 함수에서 어떤 뷰를 스냅샷 찍을지 확인하고 뷰의 이미지가 저장되는 것이다.
즉, 다시 정리해 보자면 레퍼런스 이미지를 생성하기 전에 setUp함수와 어떤 뷰로 테스트 할지 정의하는 테스트 함수를 다 작성해야 한다. 그다음 recodeMode를 true로 활성화한 상태에서 테스트를 실행하면 해당 뷰의 스냅샷이 레퍼런스 이미지로 저장된다.
실제 디자인 된 뷰로 테스트 해보고 싶을 경우 해당 폴더에 있는 스크린샷을 피그마 png로 교체한 뒤, recordMode만 false로 바꿔준 다음 동일한 코드로 다시 실행하면 저장된 레퍼런스 이미지와 실제 렌더링된 결과를 비교해서 테스트를 수행할 수 있다.
override func setUp() {
super.setUp()
self.recordMode = false // 레퍼런스 이미지 만들때만 true
}
func testMainViewSnapshot() {
let mainView = MainView()
let hostingController = UIHostingController(rootView: mainView)
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 393, height: 852))
window.rootViewController = hostingController
window.makeKeyAndVisible()
hostingController.beginAppearanceTransition(true, animated: false)
hostingController.endAppearanceTransition()
hostingController.view.setNeedsLayout()
hostingController.view.layoutIfNeeded()
let expectation = XCTestExpectation(description: "Wait for view to render")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
FBSnapshotVerifyView(hostingController.view)
}
testMainViewSnapshot 함수를 살펴보자
내가 선택한 라이브러리는 UIKit에 더 특화되어 있는 snapshot 테스트인데, 적용해 볼 프로젝트는 SwiftUI기반으로 뷰들이 구현되어 있어서 UIHostingController로 뷰를 감싼 후 UIKit 뷰 계층에 올려야 한다. 테스트하고 싶은 뷰를 생성해주지 않으면 스냅샷이 정상으로 찍히지 않는다!
window.makeKeyAndVisible()
이후 makeKeyAnyVisible()를 사용해서 호스팅 한 뷰를 key window로 지정하여 그 창을 화면에 보이도록 표시한다.
💡 KeyWindow란?
: 단순히 뷰를 보여주는 이상의 역할로, 사용자의 입력과 이벤트를 우선적으로 받을 수 있는 창을 의미한다.
뷰를 보여주기만 하면 될 경우, window.isHidden = false 값을 줄 수도 있지만, 뷰 계층을 완전히 렌더링 하기 위해 makeKeyAndVisible 함수를 호출해서 사용하면 더 안전하다.
제대로 렌더링 되지 않을 경우 레퍼런스 이미지가 하얀색 빈 EmptyView로 뜬다던가, 변수가 생기기 쉽기 때문이다!
hostingController.beginAppearanceTransition(true, animated: false)
hostingController.endAppearanceTransition()
그다음 테스트 환경에서는 뷰 컨트롤러의 lifeCycle 이벤트가 자동으로 실행되지 않을 수 있기 때문에 강제로 ViewDidAppear와 같은 lifeCycle을 강제로 호출하도록 하여 UI가 올바르게 세팅되도록 한다!
FBSnapshotVerifyView(hostingController.view)
마지막으로 FBSnapshotVerifyView를 호출하면 실제 렌더링 된 화면과 레퍼런스 이미지를 비교할 수 있게 된다!
나의 경우는 렌더링 할 때 대기 시간을 걸어주지 않으니 뷰가 EmptyView로 찍혀서 계속 테스트에 실패하였다. 그래서 DispatchQueue를 통해 대기 시간을 걸어주었더니 해결되었다!

MainView의 아이콘 이미지를 10픽셀 더 키운 후 실행해 보았다. 당연하게도 테스트는 실패한다

로그를 확인해 보면, 레퍼런스 MainView 이미지랑 실제 시뮬레이터에 돌렸을 때 MainView가 0.00% 픽셀 이상 UI가 달라서 테스트에 실패하게 된다.
FBSnapshotVerifyView함수 안에 테스트 대상 뷰만 넣을 경우 자동으로 레퍼런스 이미지와 픽셀 단위까지 정말 동일하게 테스팅이 된다.
4. 허용 오차 범위 수정
하지만 패딩값 같은 거나 lineSpacing과 같은 요소까지 px단위로 완벽하게 맞출 필요가 없거나, 디자인팀이랑 사전에 협의가 된 경우 그 범위를 설정해서 테스트가 유연하게 통과되도록 할 수 있다.

FBSnapshotVerifyView에 구현되어 있는 함수를 보면 활용할 수 있는 요소가 있다. 각 요소를 먼저 뜯어보자!
- _ view: 비교하고자 하는 뷰 객체를 전달한다.
- identifier: 테스트의 식별자 역할을 한다. 주로 같은 뷰를 여러 번 테스트할 때 충돌을 피하기 위해 사용한다
- suffixes: 레퍼런스 이미지를 찾을 때 사용할 디바이스나 환경별 접미사 역할을 한다.
- perPixelTolerance: 픽셀 단위 허용 오차를 의미한다. 초기값은 0이며 0일 경우 레퍼런스 이미지와 완전히 동일해야 통과한다.
- overallTolerance: 전체 이미지에서 허용되는 오차 비율이다.
- file: 테스트 실패 시 파일 경로를 보고하기 위해 사용된다.
- line: 테스트 실패시 라인 번호를 보고하기 위해 사용된다.
이 요소들 중에서도 허용오차 범위를 위한 요소는 perPixelTolerance, overallTolerance이므로 코드에 추가시켜 보자.
FBSnapshotVerifyView(hostingController.view, perPixelTolerance: 0.1, overallTolerance: 0.1)
허용 오차 범위는 0~1까지의 범위로 계산되며 perPixelTolerance는 각 픽셀 단위가 얼마나 안 맞는지 확인한다. 개별 컴포넌트 별로 구분되는 오차 범위이며 1로 갈수록 허용오차범위가 커지게 된다!
overallTolerance는 전체 픽셀에 대한 오차범위를 의미한다. 따라서 전체 픽셀 비율 중에 10%까지는 오차 범위를 허용하겠다~라고 코드를 작성한 상태이다! (0.01일 경우 전체 픽셀 중 총 1%까지 차이를 허용한다는 의미)

허용 오차 범위를 주고 나니 테스트에 성공한 모습을 확인할 수 있었다!
참고
https://blog.banksalad.com/tech/test-in-banksalad-ios-1/
https://github.com/pointfreeco/swift-snapshot-testing
https://github.com/uber/ios-snapshot-test-cas
https://medium.com/@ashokrwt/snapshot-testing-in-swiftui-d88640b4906d
https://medium.com/testableapple/snapshot-testing-in-xcuitest-d18ca9bdeae
'iOS' 카테고리의 다른 글
| [iOS] Moya를 활용한 네트워킹 (1) (0) | 2025.10.11 |
|---|---|
| [iOS] Instruments를 활용한 Hangs 추적과 최적화 (1) | 2025.09.01 |
| [iOS] UITest - XCUITest (3) | 2025.08.18 |
| [iOS] Swiftgen 적용하기 (1) | 2025.08.02 |
| [iOS] APNs를 통한 push notifications 구현하기 (0) | 2025.07.24 |
- Total
- Today
- Yesterday
- CoreData
- 클로저
- XCTest
- internal Combine
- Swift Format
- awakeFromNib
- SWIFT
- foundation models
- detached task
- SnapshotTest
- group tasks
- 스위프트
- swiftUI
- UIKit
- combine
- prepareForReuse
- ios
- Fastlane
- 프로그래머스
- asyne-let
- rxswift
- Task
- closure
- Swift Concurrency
- 백준
- UITest
- unstructed task
- Xcode
- ObservableObject
- 코딩테스트
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |