- 실제로 앱이랑 상호 작용하는 테스트
- 프론트엔드 또는 앱 개발을 할때는 사용자의 입력을 받게 되는데, 사용자의 입력이 제대로 처리 되는지를 테스트를 하기 위해 UI 테스트를 함
- "사용자가 어떤 workflow로 앱을 사용할 것 같은데.." -> 직접 사람이 할 수도 있지만 Xcode 에서 제공하는 UI 테스트 타겟을 사용하면 UI Test 자동화를 쉽게 구현할 수 있게 해줌
- Xcode 에서 제공하는 UI 테스트 툴은 실제 앱 타겟 내부의 코드에는 접근할 수 없지만, UI를 코드로 조작할 수 있음 (원래 Unit Test 에서는 @testable import MyProject 를 통해 프로젝트 내의 코드에 접근했음. UI Test 도 쓸 수야 있지만, 하면 안됨)
- 앱의 코드를 직접 실행하는 대신 앱의 사용자 인터페이스 컨트롤을 실제 사용자처럼 사용하여 특정 작업을 완료할 수 있는지 여부를 결정
- 핸들링이 끝나고 결과를 확인할 때는 목표로 하는 UI 요소가 존재하는지를 검색하여
XCTAssert
,XCTAssertTrue
,XCTAssertEqual
,XCTAssertNotNil
등 다양한 assertion 구문으로 판별 처리를 할 수 있음
ex) textField 에서 입력 후 버튼 누르면 해당 내용으로 alert 뜸 -> 코드로 textField 에 입력 시킨 후 버튼 누르게 하고 alert 있나 확인
-
음.. "textField 에서 입력 후 버튼 누르는걸 코드로 어떻게하지?!"
=> UI Recording은 사용자의 기기, 시뮬레이터 등의 유저 인터렉션을 코드로 생성해주는 기능을 지원
-
이 기능을 활용하면 UI 테스트를 위한 테스트 코드를 만들거나, 기존의 테스트를 확장하는 데 큰 도움이 됨
-
레코딩을 하기 위해서는 테스트 메소드 안쪽에 커서를 위치시킴. 녹화 버튼은 테스트 메소드 안에서만 활성화 되며, 테스트 메소드 바깥에서는 비활성화.
-
녹화중인 상태에서 앱에서 어떤 행동을 하면 그게 다 코드화(?) 됨. UITest는 이 UI Recording기능을 이용하면 편하게 시작 가능.
- 하지만 리팩토링이 필요하고 테스트를 검증하기 위한 코드는 직접 추가해야 함
-
그래도 UI 테스트 코드 작성에 익숙하지 않을 때 매우 유용한 방법.
-
처음에 이 상태로 그냥 실행해보면 몇번 켜지고 꺼지고가 반복됨. 이는
testLaunchPerformance
에서 퍼포먼스 측정을 위해 5번 테스트를 시행한 후 평균 시간 계산하기 위함. 주석처리하면 한번만 시행.func testLaunchPerformance() throws { if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { // This measures how long it takes to launch your application. measure(metrics: [XCTApplicationLaunchMetric()]) { XCUIApplication().launch() } } }
-
UITest 는 아래와 같은 이벤트와 응답으로 이뤄짐
- element를 찾기 위한 쿼리를 작성
- element 를 탭하거나 클릭하여 응답을 유도 (cf.
button.tap()
- iOS,button.click()
- OS X ) - 응답이 예상되는 결과와 일치하거나 일치하지 않는지 측정
-
UITest 를 위한 UI Testing API에는 크게 3가지가 있음
-
앱을 실행 및 종료할 수 있는 앱의 "프록시"
-
앱의 시작 및 종료 lifecycle과 무관. 테스트가 별도의 프로세스에서 진행중이기 때문.
즉 앱이 언제 시작되고 언제 종료할지 명시적으로 제어 가능
-
UI 테스트를 위해서는 XCUIApplication launch함수를 호출하여 앱을 실행해주어야 하는데,
setUp
에서 호출해주면 테스트 함수가 실행되기 직전에 매번 앱이 실행됨. (Launch는 "항상"새로운 프로세스를 생성하며 기존 인스턴스를 암시적으로 종료) -
하나의 테스트가 끝나고 나면 원래 상태로 돌려놔야 다음 테스트를 원활하게 할 수 있기 때문에
XCTestCase
의tearDown
메소드를 오버라이딩 하여 테스트 리셋을 시킴.이때 테스트마다 복구하는 방법이 다를 수 있기 때문에 테스트 메소드마다
addTeardownBlock(_:)
함수를 사용하여 테스트 종료시 복구하는 방법을 다르게 선언해 주는 것이 가능 (Unit Test 에서도 사용 가능 )
-
XCUIElement는 UI 테스트에서 UIButton 또는 UILabel 등의 컴포넌트를 대신하는 객체
-
주로 type 과 accessibility identifier / label 의 조합으로 element 를 찾게 됨 (ex. "titleTextField" identifier 를 가진 textField )
아래 예시에서는 textField가 XCUIElement타입
-
XCUIApplication의 표현을 빌리자면, 앱 안의 UI element에 대한 프록시
-
UI 테스트는 사람이 실제로 앱을 사용하는 것처럼 현재 실행 중인 앱에서 필요한 UI 컴포넌트를 찾아 Tap을 하거나, 텍스트를 입력하고 스크롤을 하는 등의 동작을 구현해야 함.
따라서 Element 존재 여부인 exists, 터치를 위한 tap, 텍스트 입력을 위한 typeText 함수가 주로 사용하는 XCUIElement의 대표적인 Property 및 함수
-
XCUIElement 는 XCUIElementAttributes 를 conform 하고 있는데 여기에는 value, title, label, placeholderValue, isEnabled, isSelected, frame 등이 있음.
-
사실 XCUIApplication 도 XCUIElement 를 상속
-
화면에 그려진 XCUIElement를 찾기 위해서는 XCUIElementQuery 를 사용할 수 있음.
-
즉 UI element를 찾기 위한 쿼리
아래 예시에서는 textFields 가 XCUIElementQuery
-
컴포넌트를 찾는 원리는 매우 간단. 왜냐하면 뷰는 트리 형태로 구성되어 있기 때문. 아래 그림을 참고하면 Element를 찾는 방식을 쉽게 이해 가능
-
쿼리만 쓴다고 무조건 XCUIElement 가 나오는게 아님. 아래 코드에서 table과 cell은 여전히 XCUIElementQuery타입
-
아래 코드는 현재 View안의 table과 cell을 전부 가져오는 코드
UITest 를 위해서는 유니크한 하나의 View를 찾고 상호작용 해야하기 때문에 identifier 등을 통해 더 명확하게 쿼리를 줘야함
-
query 로 element 를 찾으려면 identifier 를 이용하는 것 외에도, index 로 찾을수도 있고, element 가 unique 하다는걸 알 경우에는 그냥 element 로 찾을 수도 있음
-
query 는 실제 사용할 때, 즉 XCUIElement 에 tap 등의 이벤트가 발생한다든가, property value 를 읽는다던가 하는 상황에서 on demand 하게 평가 (evaluated) 됨 (생성만으로 fetch 안하는 URL 과 비슷).
-
XCUIElement 를 가져오는 방법은 위에서 살펴봤듯이 다양하지만, 가장 중요한건 Accessibility label(또는 Accessibility identifier)로 UI Element를 식별한다는 점.
-
UI Test에서의 코어 기술은 XCTest 프레임워크와 Accessibility의 조합
-
Query 는 Accessibility 의 관점에서 visible 한 element 만 찾을 수 있음 (WWDC 2015 - UI Testing in Xcode)
-
따라서 통합 UI 테스트 도입을 위해서는 앱의 접근성 작업이 선행되어야함. 왜냐하면 UITestRunner는 앱에서 제공해 OS에 노출된 “접근성 트리”를 활용해 버튼을 찾고 그것과 상호작용하기 때문.
컴퓨터에는 눈이 없으니까 당연히 시각장애인이 앱을 사용하는 것과 같은 방식으로밖에 앱을 사용할 수 없음. 즉, VoiceOver로 사용할 수 없는 버튼은 테스트코드로도 사용할 수 없음.
-
Accessibility 세팅은 스토리보드 등에서 Accessibility label(또는 Accessibility identifier도 가능)을 지정 가능.
-
matching(identifier:)
나 subscript 로 찾을 때 넘기는 값은 accessibility Identifier 나 accessibility label 둘 다 됨 -
그럼에도 UI Test 는 accessibility identifier 를 이용하는게 좋음.
왜냐하면 accessibilityLabel이 최종 사용자를 대상으로 하는 반면, accessibilityIdentifier는 개발자 전용이기 때문에 로컬라이징이 필요 없고, 되어서도 안되기 때문.
만약 accessibilityLabel 을 기준으로 element 를 찾는다면 해당 값이 바뀌거나 다른 나라 유저가 쓰는 로컬라이징 된 앱일때 해당 element 를 못찾게 됨
-
만약 accessibility identifier, label 둘 다 없으면 기본적으로 애플에서 Accessibility label 을 눈에 보이는 text 로 설정하기 때문에 해당 값으로 찾을 수 있음
accessibility identifier, label 둘 중 하나라도 설정되어있을 때
accessibility identifier, label 둘 중 모두 설정 안되어 있을 때
-
app.buttons[”제출”].tap()
은 동작하지 않고,app.otherElements[”제출”].tap()
은 동작하는 경우가 있음시행착오를 거치지 않고 처음부터 적절한 XCUIElementQuery를 쓰기 위해서는 VoiceOver를 활용 할 수 있음. VoiceOver에서 그 버튼을 “버튼”이라고 불러준다면,
app.buttons
은 동작. 만약 VoiceOver와 테스트코드에서 이 버튼을 부르는 방식을 바꾸고 싶다면 버튼의 accessibilityTrait을 수정 -
만약 현재 View에 해당 Accessibility label(또는 Accessibility identifier)을 가진 요소가 없는데 tap() 등을 하려고 한다면 테스트는 실패.
하지만 exist 를 사용할때는 있냐 / 없냐를 체크하는거기 때문에 해당 element 가 없다고 해서 무조건 실패하지는 않음
-
동일한 Accessibility label(또는 Accessibility identifier)를 가지고 있는 요소가 여러개 있어도 테스트는 실패
-
테스트 코드를 작성하기 전, 테스트 목표와 시나리오를 미리 작성해두면 매우 큰 도움이 됨
시나리오는 실제 앱을 사용하는 패턴으로 최대한 꼼꼼하게 작성하는 것이 좋음. 왜냐하면, 시나리오를 그대로 코드로 옮기면 테스트를 완성할 수 있기 때문
-
Test함수 이름은 'test'라는 단어로 시작해야 한다는 규칙이 있음 (Unit Test 와 동일)
XCTestCase를 상속받은 테스트 클래스에 'test'로 시작하는 함수는 모두 테스트 함수로 인식되기 때문에 test 이후 아무렇게나 작성해도 되지만, 보통은 '테스트하려는 기능 또는 목적'을 뒤에 붙여주는 형태로 이름을 지음
- Github의 API를 이용하여 사용자를 검색하여 테이블 뷰에 보여주는 앱
- 검색어에 실시간으로 결과를 요청하고 페이지네이션을 지원
- 테이블 셀을 터치하면 상세 화면으로 이동
- SearchBar의 TextField를 탭
- 검색어 입력
- 검색 결과가 있는지 확인
func test_SearchUserResultAvailable() {
let searchField = app.searchFields.firstMatch
XCTAssertTrue(searchField.exists) // 검색창의 TextField가 있는지 확인
searchField.tap() // 1. SearchBar의 TextField를 탭
searchField.typeText("Mildwhale\n") // 2. 검색어 입력 후 키보드 내림
let resultCellOfFirst = app.cells.firstMatch
XCTAssertTrue(resultCellOfFirst.waitForExistence(timeout: 15.0)) // 3. 검색 결과가 있는지 확인 (최대 15초 동안 대기)
}
- SearchBar의 TextField를 탭
- 검색어 입력
- 검색 결과가 있는지 확인
- 검색어 삭제
- 검색 결과가 있는지 확인 (없어야 한다)
- SearchBar의 TextField를 탭
- 검색어 입력
- 검색 결과가 있는지 확인
- 첫 번째 셀 터치가 가능한지 확인
- 첫 번째 셀을 터치
- 상세 화면으로 이동하는지 확인
- SearchBar의 TextField를 탭
- 검색어 입력
- 검색 결과가 있는지 확인
- 테이블 뷰를 상단으로 스크롤
- 다음 페이지가 테이블 뷰의 하단에 추가되었는지 확인
나머지 코드를 보고 싶다면 - UITest_SearchUser.swift
- 특정 아이템 번호를 넣었을 때 비동기 작업을 처리하여 테이블에 추가하는 앱
- 네비게이션 바에 있는 특정 버튼을 탭하면, 아이템 번호를 넣을 수 있는 alert 가 뜸
-
네비게이션 바에 있는 특정 버튼을 탭
-
노출되는 Alert 내부의 Text Field에 Text를 입력
-
확인 버튼 탭
-
비동기 작업을 처리하기 위해 추가 대기 시간을 주고 Expectation이 완료되는 것을 기다림
만약 비동기 요소가 있어서 대기를 해야 할 경우가 생긴다면 유닛 테스트에서 했던 것과 유사하게
waitForExpectation(timeout:handler:)
또는wait(for:timeout:)
를 사용하면 원하는 시간만큼 테스트 흐름을 대기 시킬 수 있음 -
테스트가 끝나면
addTeardownBlock
내에서 선언 한 초기화 작업을 수행하고 테스트를 종료
func testAddItemWithWriteButton() {
let app = XCUIApplication()
let targetText = "Item No.\(TestItemNumber)"
// UI 자동 조작
app.navigationBars["아이템 목록"].buttons["icon edit"].tap()
let alert = app.alerts["아이템 번호 입력"]
alert.collectionViews.textFields["ex) 1234567"].typeText("\(TestItemNumber)")
alert.buttons["확인"].tap()
// 비동기 작업 대기 후 체크
let predicate = NSPredicate(format: "exists == true")
let promise = expectation(for: predicate,
evaluatedWith: app.tables.cells.staticTexts[targetText],
handler: nil)
wait(for: [promise], timeout: TimeoutSeconds)
addTeardownBlock {
app.tables.cells.staticTexts[targetText].firstMatch.swipeLeft()
app.buttons["Delete"].firstMatch.tap()
}
}
- 주로 아래와 같이 XCUIElement 의 프로퍼티인
exists
를 통해 element의 존재 여부 많이 판단하는듯
func testAccessibilityLabel() throws {
let myButton = app.buttons.matching(identifier: "접근성 텍스트").element
XCTAssert(myButton.exists)
}
- 아니면 exists 대신 isHittable 통해 실제로 user 가 hit 할 수 있는 element 인가 체크 (modal 등에 가리지 않고)
XCTAssertTrue(myButton.isHittable)
-
실제 디바이스(시뮬레이터)로 테스트하기 때문에 일반적으로 약간의 여유를 두는 것이 좋음. 예를 들어 애니메이션이 발생할 수 있음. 따라서 exists를 바로 사용하는 대신 waitForExistence(timeout:)를 사용하는 것이 일반적
XCTAssertTrue(app.alerts["Warning"].waitForExistence(timeout: 1))
해당 element가 즉시 존재하면 메소드가 즉시 반환되고 테스트가 통과되지만, 그렇지 않으면 메소드는 최대 1초까지 대기
po myButton
찍어봤을때 Element subtree 에 accessibility label 이 "Alert 띄우기!" 인 StaticText 가 있다는 것 확인
그럼 myButton
의 하위 staticTexts 타입들 중에 "Alert 띄우기!" accessibility label 으로 접근 가능한 element 가 있는지 보면 되겠다!
그럼 뭐.. 이런식으로 가능!
XCTAssert(myButton.staticTexts["Alert 띄우기!"].exists)
XCTAssertEqual(myButton.staticTexts.element.label, "Alert 띄우기")
사실 이 화면에서는 해당 element 만 "Alert 띄우기" 값을 표현하고 있기 때문에 그냥 app.staticTexts["Alert 띄우기!"]
처럼 바로 찾아도 됨.
하지만 똑같은 값을 가진 label 을 하나 더 올린다?
그러면 Multiple matching elements 라고 중복된 element 있다고 바로 에러나버림
많은 사람들이, “주어진 조건에서 원하는 정보가 원하는대로 출력되는지”를 테스트하기 위해 “ViewModel”을 테스트하라고 조언. ViewModel에서 출력이 기대한대로 나오면, ViewModel과 View가 제대로 연결되어있다는 가정 하에, View에는 기대한 출력이 그대로 반영될 것이기 때문. 따라서 이런 값 할당 등의 확인은 Unit Test 가 난듯.
질문: "Requested -> 회색, Draft -> 노란색 인거 UITest 로 체크할 수 있냐?" or "view 의 background 색상 확인할 수 있냐?"
답변: 유감.
UI 테스트는 접근성이 지원하는 볼 (see) 수 있는 범위에서만 인터페이스를 확인할 수 있음.
따라서 텍스트를 "볼" 수는 있지만 이 항목이 UILabel 인 것을 "볼" 수는 없음. 따라서 UILabel로서의 특성을 탐색할 수 없음
비슷한 맥락으로 백그라운드 색상 확인도 불가능.
따라서 색상 확인하고 싶으면 Unit Test 를 사용해서 뷰를 초기화하고 배경색을 확인하든가, UITest 에서 screenShot 찍어서 직접 확인하든가..?
참고
UITest color of a label (not UI label)
How to check background color of the XCUIElement in UI test cases??
In XCTest UI Testing, how to check the background color of button, label , views?
-
Xcode 9 부터 병렬 테스트를 지원 해서 동시에 2개 이상의 기기에서 테스트 진행 가능
-
UI 테스트에 사용하는 시뮬레이터의 개수는 시스템의 성능에 따라 자동으로 결정
-
터미널 명령어 xcodebuild로 Runner(worker) 의 개수를 직접 설정할 수도 있지만, 너무 많은 개수를 지정하면 시뮬레이터를 실행하는 데 모든 자원을 사용해버려서 테스트가 진행되지 않음. 빌드머신이 감당할 수 있는 최대의 개수를 지정해주는 것이 가장 좋음.
-parallel-testing-worker-count n
-
병렬화 테스트는 Class 단위로 테스트를 진행. 즉, 하나의 Class에 너무 많은 테스트 함수가 구현되어 있다면 이것을 적절히 나누어 주는 것이 좋음. 시뮬레이터는 3개인데 테스트 함수가 1개의 Class에 모두 구현되어있다면, 병렬 테스트의 장점을 전혀 살리지 못함
- Edit Scheme 메뉴 진입
- Test 메뉴 선택
- 병렬화하고자 하는 Tests의
Options...
선택 Execute in parallel on Simulator
에 체크
-
UI 테스트는 앱이 예상대로 사용자에게 작동하는 궁극적인 지표이지만 다른 종류의 테스트보다 실행하는 데 시간이 더 오래 걸림
-
동일한 UI 테스트에서 실패를 유발할 수 있는 다양한 앱 변수가 있음
예를 들어 아래처럼 레이아웃을 맞춰놨는데, se 같은 작은 기기에서는 키보드가 "alert 띄우기!" 버튼을 가릴 수 있음.
그 상태에서 해당 버튼을 .tap() 하려고 한다면, 탭할 수 없다고 에러남;;
UI Test 실패하면 실패 상태를 Report Navigator 에서 screen shot 으로 볼수 있음
아니면 textField 에 h.tap(), i.tap() 을 한다고 작성해뒀는데, 기본 자판이 한글이라 테스트가 실패할 수도 있음
-
이처럼 통합 UI테스트의 유지 보수 비용은 비쌈.
-
실제 앱을 대상으로 하는 통합UI 테스트는 당연히 서버에 API를 쏠 때도 진짜 서버에 쏘기 때문에 실패할 여지가 굉장히 많고 그만큼 다루기 어려움.
-
그렇기 때문에 통합 UI 테스트만으로 앱의 모든 기능을 구석구석 수시로 테스트하는 것은 비현실적. 하지만 반드시 꼼꼼하고 확실하게 테스트해야 하는 영역, “이 기능이 망가지면 회사가 망한다”고 생각되는 영역들에 대해서 만큼은, 통합 UI테스트를 작성해야만 함.
UITest (2) - Recording UI Test
UITest (3) - XCUIApplication / XCUIElement / XCUIElementQuery
accessibilityLabel / accessibilityIdentifier
[iOS] Xcode를 이용한 UI 테스트 - 1. 시작하기
[iOS] Xcode를 이용한 UI 테스트 - 2. 테스트 케이스 작성
[iOS] Xcode를 이용한 UI 테스트 - 3. 테스트 녹화
[iOS] Xcode를 이용한 UI 테스트 - 4. Tip & Tricks for UI Testing
Github-SearchUser-Tutorial.swift
[Unit / UITest] Parallel Testing으로 Test시간을 줄여보자
뱅크샐러드 iOS팀이 숨쉬듯이 테스트코드 짜는 방식 1편 - 통합 UI테스트
아, 나도 테스트 코드 짠다! - (2) Xcode UI 테스트 작성
뱅크샐러드 iOS팀이 숨쉬듯이 테스트코드 짜는 방식 2편 - 화면 단위 통합 테스트
애플 공식 문서
테스팅 관련 WWDC
WWDC 2015 - UI Testing in Xcode
WWDC 2018 - What's New in Testing
WWDC 2017 - What's New in Testing
WWDC 2017 - Engineering for Testability