XCTestCase

요즘에는 SwiftTest라고 SwiftUI에서 하는 테스트가 따로 있던데 우리 회사는 UIKit을 기반으로 Objc / Swift를 혼합하여 사용하고 있어 XCTest를 사용해야 했다.


WWDC 즈음부터 공부하던 거라 ㄱ- 알고 보니 SwiftTest는 2024WWDC에서 새로 나온 거더라?! 전혀 몰라따 .... :3

 

 

XCTest의 처음과 끝

먼저, 기본적인 순서는 다음과 같다.

setUp(): 앱 실행
test_something(): 기능 테스트
tearDown(): 종료


첨에 이런 순서를 알고 테스트를 했는데 갑자기? 막 오류가 나고? 이상하게 테스트가 진행되는 것이다.


그러니까, 나는 … …

 

setUp() → test_1() → test_2() → … → tearDown() 이런 순인 줄 알았는데 아니었다.

 

 


바로바로~ 이런 식이었던 것이다.


테스트 하나가 실행된 이후에는 다시 앱이 종료되고 새로운 application 객체로 다른 테스트를 실행하는 것이당.

 

기존에 코드에 들어 있는 continueAfterFailure 는?

실패한 후에도 테스트 method가 계속 실행되어야 하는지 여부를 나타내는 프로퍼티로, 기본값은 True이다.
애플에서는 UI 테스트에서 오류가 발생하면 즉시 중지하는 것이 가장 좋다고 말하고 있다!

 

 

자동화 테스트 함수를 선언하기

모든 자동화 함수들은 test_아무거나 이런 식으로 선언되어야 한다.

그래야 위에서 말한 대로 앱 실행과 종료 사이에 해당 함수만 실행된당.


여기저기 많이 찾아보고 공부하고 했는데도 많이 헤맸던 부분이 여기였는데, 세부 로직에 사용되는 함수에도 test_ 로 선언하면 해당 함수가 끝난 이후에 앱도 종료되기 때문에 로직이 꼬이기 십상이다.

 

그러니까, 큰 자동화 함수를 선언할 때는 사용자의 연속적인 행동이 들어가 있는 것이 좋다. 만약에 분기가 나뉘거나 다른 부분을 테스트하게 된다면 역시 다른 함수로 빼는 것이 좋고!

 

나는 자동화 테스트 함수를 만들기 위해 전체적인 시퀀스를 만들어 사용자의 행동에 따라서 UI가 원활히 동작하는지 테스트했다.

 

 

세부 요소들

UITestingAPI

  • XCUIApplication
    • 앱을 실행 / 종료할 수 있는 앱의 프록시로, 테스트는 별도의 프로세스에서 진행 중이기 때문에 언제 시작되고 언제 종료될지 명확하게 작성되어야 함
  • XCUIElement
    • 사용자의 Action을 받기 위해 존재하는 UI요소들을 말한다.
      • 일반적으로 tap(), click(), 스와이프 제스쳐 등을 동작하게 만들 수 있다.
    • 앱의 현재 UI 계층 구조 내에 특정 요소가 존재하는지 확인하려면 exists 인스턴스 속성을 사용하여 확인할 수 있다.

 

  • XCUIElementQuery
    • UIElement를 찾기 위한 쿼리로, 종류가 매우 다양하다. XCTest 내의 XCUIElementQuery.h를 살펴보면 .. ..어마어마하게 많다.
    • @property(readonly, copy) XCUIElementQuery *statusItems; @property(readonly, copy) XCUIElementQuery *otherElements; @property(readonly, copy) XCUIElementQuery *handles; @property(readonly, copy) XCUIElementQuery *layoutItems; @property(readonly, copy) XCUIElementQuery *layoutAreas; @property(readonly, copy) XCUIElementQuery *cells; @property(readonly, copy) XCUIElementQuery *levelIndicators; @property(readonly, copy) XCUIElementQuery *grids; @property(readonly, copy) XCUIElementQuery *rulerMarkers; @property(readonly, copy) XCUIElementQuery *rulers; @property(readonly, copy) XCUIElementQuery *dockItems; @property(readonly, copy) XCUIElementQuery *mattes; @property(readonly, copy) XCUIElementQuery *helpTags; @property(readonly, copy) XCUIElementQuery *colorWells; @property(readonly, copy) XCUIElementQuery *relevanceIndicators; @property(readonly, copy) XCUIElementQuery *splitters; @property(readonly, copy) XCUIElementQuery *splitGroups; @property(readonly, copy) XCUIElementQuery *valueIndicators; @property(readonly, copy) XCUIElementQuery *ratingIndicators; @property(readonly, copy) XCUIElementQuery *timelines; @property(readonly, copy) XCUIElementQuery *decrementArrows; @property(readonly, copy) XCUIElementQuery *incrementArrows; @property(readonly, copy) XCUIElementQuery *steppers; @property(readonly, copy) XCUIElementQuery *webViews; @property(readonly, copy) XCUIElementQuery *maps; @property(readonly, copy) XCUIElementQuery *menuBarItems; @property(readonly, copy) XCUIElementQuery *menuBars; @property(readonly, copy) XCUIElementQuery *menuItems; @property(readonly, copy) XCUIElementQuery *menus; @property(readonly, copy) XCUIElementQuery *textViews; @property(readonly, copy) XCUIElementQuery *datePickers; @property(readonly, copy) XCUIElementQuery *secureTextFields; @property(readonly, copy) XCUIElementQuery *textFields; @property(readonly, copy) XCUIElementQuery *staticTexts; @property(readonly, copy) XCUIElementQuery *scrollBars; @property(readonly, copy) XCUIElementQuery *scrollViews; @property(readonly, copy) XCUIElementQuery *searchFields; @property(readonly, copy) XCUIElementQuery *icons; @property(readonly, copy) XCUIElementQuery *images; @property(readonly, copy) XCUIElementQuery *links; @property(readonly, copy) XCUIElementQuery *toggles; @property(readonly, copy) XCUIElementQuery *switches; @property(readonly, copy) XCUIElementQuery *pickerWheels; @property(readonly, copy) XCUIElementQuery *pickers; @property(readonly, copy) XCUIElementQuery *segmentedControls; @property(readonly, copy) XCUIElementQuery *activityIndicators; @property(readonly, copy) XCUIElementQuery *progressIndicators; @property(readonly, copy) XCUIElementQuery *pageIndicators; @property(readonly, copy) XCUIElementQuery *sliders; @property(readonly, copy) XCUIElementQuery *collectionViews; @property(readonly, copy) XCUIElementQuery *browsers; @property(readonly, copy) XCUIElementQuery *outlineRows; @property(readonly, copy) XCUIElementQuery *outlines; @property(readonly, copy) XCUIElementQuery *tableColumns; @property(readonly, copy) XCUIElementQuery *tableRows; @property(readonly, copy) XCUIElementQuery *tables; @property(readonly, copy) XCUIElementQuery *statusBars; @property(readonly, copy) XCUIElementQuery *toolbars; @property(readonly, copy) XCUIElementQuery *tabGroups; @property(readonly, copy) XCUIElementQuery *tabBars; @property(readonly, copy) XCUIElementQuery *tabs; @property(readonly, copy) XCUIElementQuery *navigationBars; @property(readonly, copy) XCUIElementQuery *keys; @property(readonly, copy) XCUIElementQuery *keyboards; @property(readonly, copy) XCUIElementQuery *popovers; @property(readonly, copy) XCUIElementQuery *toolbarButtons; @property(readonly, copy) XCUIElementQuery *menuButtons; @property(readonly, copy) XCUIElementQuery *comboBoxes; @property(readonly, copy) XCUIElementQuery *popUpButtons; @property(readonly, copy) XCUIElementQuery *disclosureTriangles; @property(readonly, copy) XCUIElementQuery *checkBoxes; @property(readonly, copy) XCUIElementQuery *radioGroups; @property(readonly, copy) XCUIElementQuery *radioButtons; @property(readonly, copy) XCUIElementQuery *buttons; @property(readonly, copy) XCUIElementQuery *dialogs; @property(readonly, copy) XCUIElementQuery *alerts; @property(readonly, copy) XCUIElementQuery *drawers; @property(readonly, copy) XCUIElementQuery *sheets; @property(readonly, copy) XCUIElementQuery *windows; @property(readonly, copy) XCUIElementQuery *groups; @property(readonly, copy) XCUIElementQuery *touchBars;

 

이 Query를 이용해서 내가 원하는 UIElement를 골라내야 하는데, 그럼 어케 골라내냐? 가 젤 큰 문제였다.


보이는 Text로 골라낼 수도 있지만, 만약에 텍스트가 없는 버튼이라면? 사진이라든가 아이콘이 들어가 있다면 해당 부분이 보이는지는 확인할 수 없을 것이다.


사실 텍스트가 있어도 그 텍스트가 언제든 달라질 수 있지 않은가?! 뭐 Localization이라등가 기획상의 변경으로 언제는 (여기누르세요) 였던 게 (Press This Button)이 될 수도 있는 거고 (여기클릭)이 될 수도 있다.

 

그래서~ 사용하는 것이~
AccessibilityLabelAccessibilityIdentifier이다.

 

 

근데 갑자기 Accessibility?! 라는 생각이 들텐데 (사실제가그랫어요)


UI 객체를 가져올 때는 크게 3가지로 가져온다고 생각하면 편하다.

  1. Text (버튼의 title, label의 text … … etc)
  2. AccessibilityLabel
  3. AccessibilityIdentifier

AccessibilityIdentifier가 설정되지 않았을 때는 자동으로 AccessibilityLabel로 가져온다.

 


다만, 테스트를 목적으로 AccessbilityLabel을 설정하는 것은 권장하지 않는다.

VoiceOver 사용자들에게 직접적으로 읽어주는 텍스트인 만큼 해당 부분은 정말 사용자가 실제로 들었을 때 도움이 되는 부분이 들어가야 한다.
→ 그럼 뭐 써야 대는데요?
AccessibilityLabel 쓰자!! (그러라고잇슴)

 

이게 해 보니까 간혹 나는 AccessbilityIdentifier의 Text로 인식하겠지? ㅇ.ㅇ 하고 써놔도 버튼의 Text와 겹치는 게 있다면 테스트 중에 뭘 가지고 와야 할지 몰라 오류가 나는 경우가 간혹 있더라.


나는 그런 경우가 무작위 버튼이었는데,
버튼의 title과 AccessbilityIdentifier가 같은 버튼들이 있다고 하자.


그럼 뭐가 문제겠는가?


또 identifier를 동일하게 설정해야 하는 버튼들이 없다면 이대로 써도 좋을 것이다. (별로 추천하진 않는다.)

 

 

근데 만약 indentifier를 이런 식으로 짓는다면?

 

 

 

아니면 버튼의 title이 무작위로 정해진다면?

 

 

 

 

그럼 컴파일러는 어? 나 어디서 가져오지? 누구 가져와야 하지? 하고 에러를 뱉을 것이다.

 

따라서, identifier는 다른 title 및 accessibilityLabel과 겹치지 않게 정해야 한다.

 

 

 

 

더이상 미룰수없다. 블로그 글 올리기.

이 글을 쓰기 시작했던 게 아마 두세 달 전으로 기억하는데 ... ... ㅜ.ㅜ

혼자 열시미 정리하다가 못 올릴 것 가타서 일단 올린다.
자세한 테스트 로직은 2에서.ᐟ.ᐟ.ᐟ.ᐟ.ᐟ.ᐟ.ᐟ.ᐟ.ᐟ.ᐟ.ᐟ.ᐟ

 

 

 

 

 

 

 

참고 자료

https://stackoverflow.com/questions/33247116/testing-if-an-element-is-visible-with-xcode-7-uitest/71441971#71441971
https://stackoverflow.com/questions/41442932/ios-xcuitests-access-element-by-accessibility
https://medium.com/@robert.crabtree/ios-ui-testing-with-xctest-accessibility-8166a8bac2e1

https://developer.apple.com/videos/play/wwdc2023/10035/

https://developer.apple.com/documentation/xctest/user_interface_tests
https://developer.apple.com/documentation/xctest/xcuielementquery
https://medium.com/quality-engineering-university/xcuitests-how-to-find-matching-element-76e65371fe74
https://zeddios.tistory.com/1064