iOS 테스트와 TDD
테스트 종류
Unit Test
- 단위 코드의 로직적인 부분을 테스트
- 예를들어, 개별적인 함수가 예상대로 작동하는지 등을 확인
- 각 테스트 케이스는 독립적으로 실행되어야 함
- Given → When → Then 순서로 테스트 작성
Integration Test
- 모듈이나 시스템의 부분들이 상호작용을 잘 하는지 검증
- Unit 테스트와 비슷하지만, 테스트하는 코드의 범위가 더 큼
- 이 단계에서 Reference와 비교하는 Snapshot 테스팅을 하기도 함
- Snapshot 테스팅을 위해
swift-snapshot-testing
와 같은 라이브러리 활용
UI Test
- 앱의 UI가 사용자와의 상호작용 중에 올바르게 작동하는지 검증
- 사용자의 상호작용을 자동화하고 시뮬레이션하여 UI의 요소들이 예상대로 작동하는지 확인
Hammer
와 같은 라이브러리 활용
테스트의 기본
XCTestCase
XCTestCase
는 XCTest 프레임워크에서 제공하는 클래스- 모든 테스트 케이스의 기본이 되는 클래스이다
XCTestCase
를 상속 받아서 테스트 작성하기 시작
import XCTest
final class MyUITests: XCTestCase { ... }
setUp()
와tearDown()
으로 테스트 라이프 사이클 관리
override func setUp() { ... }
override func tearDown() { ... }
setUpWithError()
와tearDownWithError()
로 에러 핸들링 가능
override func setUpWithError() throws { ... }
override func tearDownWithError() throws { ... }
- 다음
setUp
과tearDown
으로 async 코드도 사용 가능
override func setUp() async throws { ... }
override func tearDown() async throws { ... }
@testable
@testable import MyApp
@testable
을 사용해import
하면 테스트 클래스에서open
,public
,internal
에 접근 가능- UI 테스트에서는 의도적으로 이를 사용하지 않아 내부 코드를 접근하지 않고, UI 컴포넌트만 접근
Assert 메소드
- 동일성:
XCTAssertEqual
,XCTAssertNotEqual
- Boolean:
XCTAssertTrue
,XCTAssertFalse
- Nullable:
XCTAssertNil
,XCTAssertNotNil
- 비교:
XCTAssertLessThan
,XCTAssertGreaterThan
,XCTAssertLessThanOrEqual
,XCTAssertGreaterThanOrEqual
- 에러:
XCTAssertThrowsError
,XCTAssertNoThrow
UI 가져오기
accessibilityIdentifier
를 사용해 접근성 ID를 지정할수 있다- 이 ID로 테스트에서 해당 UI를 접근할수 있다
// SwiftUI 코드
ProfileHomeMomentView().accessibilityIdentifier("ProfileHomeMomentView")
// UIKit 코드
uiview.accessibilityIdentifier = "ProfileHomeMomentView"
// 테스트 코드
app.otherElements["ProfileHomeMomentView"]
- Xcode console에서
break point
와po
명령어로 UI id 혹은 label 확인 가능
(lldb) po app.otherElements
Output: {
Other, 0x105832cb0, {{0.0, 0.0}, {393.0, 852.0}}
Other, 0x1058196d0, {{0.0, 0.0}, {393.0, 852.0}}
Other, 0x105812c80, {{0.0, 0.0}, {393.0, 852.0}}
...
Other, 0x1058103a0, {{16.0, 61.7}, {28.0, 28.0}}, label: 'gearshape.circle.fill'
Other, 0x10580f930, {{337.0, 57.7}, {40.0, 36.0}}, label: 'More'
Other, 0x105809590, {{0.0, 544.0}, {393.0, 697.0}}, identifier: 'ProfileHomeMomentView'
}
PageObject Pattern
- 주로 자동화된 UI 테스트에 사용되는 디자인 패턴
import XCTest
class ProfileHomeScreen {
var app: XCUIApplication
init(app: XCUIApplication) {
self.app = app
}
var profileTab: XCUIElement {
let tabBar = app.tabBars["Tab Bar"]
return tabBar.buttons["My"]
}
var momentView: XCUIElement {
app.otherElements["ProfileHomeMomentView"]
}
}
UI Test 예시
- PageObject Pattern을 사용한 간단한 UI Test 예시
import XCTest
final class MyUITests: XCTestCase {
var profileHomeScreen: ProfileHomeScreen!
override func setUpWithError() throws {
profileHomeScreen = ProfileHomeScreen(app: XCUIApplication())
profileHomeScreen.app.launch()
}
func testGivenAccessLevel_whenFriends_thenProfileMomentExists() {
profileHomeScreen.profileTab.tap()
XCTAssertTrue(profileHomeScreen.momentView.exists)
}
}
테스트 팁
좋은 테스트 조건
F.I.R.S.T
- Fast: 빠른
- Independent: 독립적인
- Repeatable: 반복 가능한
- Self-Validating: 자체 검증 가능한
- Timely: 적시의
좋은 명명법
func test_givenAppModel_whenStarted_thenInProgressState()
- 모든 테스트는
test
로 시작해야 한다 givenAppModel
:AppModel
이 SUT(system under test)임을 나타낸다whenStarted
: 테스트의 조건 또는 상태 변화이다thenInProgressState
: SUT의 상태가 어떠해야 하는지에 대한 확인이다
Test Coverage
- 좌측 Test Navigator에서
+ 버튼
을 눌러 새로운xctestplan
파일을 만든다 - Test Plan의 Tests에서 기존의 Test 타겟 추가
- Test Plan의 Configurations에서 Code Coverage를 On으로 변경 및 타겟 설정
- 메뉴바
Product → Scheme → Edit Scheme
의 Test에서 해당 Test Plan을 디폴트로 지정
테스트 빌드 확인
launchArguments
를 통해 테스트시에만 필요한 코드를 실행할수 있다.
// 테스트 코드
class MyAppUITests: XCTestCase {
override func setUp() {
let app = XCUIApplication()
app.launchArguments = ["UITests"]
app.launch()
}
}
// 일반 코드
if ProcessInfo.processInfo.arguments.contains("UITests") { ... }
스피드업 팁
- speed 값을 조절해 테스트를 빠르게 진행할 수 있다
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if ProcessInfo.processInfo.arguments.contains("UITests") {
UIApplication.shared.keyWindow?.layer.speed = 100
}
}
class MyAppUITests: XCTestCase {
override func setUp() {
let app = XCUIApplication()
app.launchArguments = ["UITests"]
app.launch()
}
}
XCUIElement 팁
- VoiceOver를 활성화하고 해당 Element를 탭하면 어떤 종류인지 알 수 있다.
- Element의 종류를 바꾸고 싶다면 버튼의
accessibilityAddTraits
를 사용할 수 있다.
시간차 대응
- 애니메이션이나 네트워크 통신 등으로 UI가 나타나기까지 시간차가 발생하는 경우가 있다.
- 그럴때는
waitForExistence
를 적절히 사용하자.
XCTAssertTrue(login.getStartedButton.waitForExistence(timeout: 2))
Xcode Cloud
- 여러 종류의 테스트를 Xcode Cloud로 간편하게 자동화 할 수 있다
- 기회가 되면 다른 글을 통해 더 자세히 알아보겠다
TDD 프로세스
- Red Test: 실패하는 테스트를 만든다
- Green Test: 테스트가 패스하도록 수정한다
- Refactor: 실제 코드와 테스트 코드를 리팩토링 한다
- Repeat: 필요한 테스트가 다 만들어질때까지 이 과정을 반복한다