티스토리 뷰

iOS

[iOS] UITest - XCUITest

yanni13 2025. 8. 18. 01:37

 

 

이번 포스팅에서는 UITest에 대해서 다뤄볼 예정이다!

사용자 인터렉션을 테스트하여 UI플로우를 검증하는 XCUITest와,

실제 픽셀 단위로 UI를 제대로 구성했는지 확인해 볼 수 있는 Snapshot Test에 대해서 공부해 보자.

 

 

🧐 테스트 코드의 필요성

왜 Test-code를 사용해야 할까?

 

주변에서 테스트 코드를 프로젝트에 도입한다고 하면 개발하기도 바쁜데 테스트 코드까지 짜면 시간낭비 아니냐, 비효율적인 거 같다는 말을 많이 들었다.

 

나 또한 그렇게 생각했었고 여전히 프로젝트 규모가 작거나, 단기성 프로젝트일수록 테스트 코드를 도입하는 것이 비효율적이라고 생각한다.

하지만 장기전으로 봤을 때 테스트 코드가 동작하는 기능은 테스트가 동작하지 않는 기능보다 나중에 리펙토링에서 훨씬 부담을 덜 느끼게 수정할 수 있을 것이다. 

 

기획이 변경되거나 디자인 플로우가 변경되어 기존 기능을 수정하게 되었을 때 이 코드를 수정함으로써 생기는 버그와 이슈들은 예상할 수 없을 것이다. 그걸 일일이 찾아낼 수도 있겠지만, 규모가 큰 프로젝트 일 경우 전체 앱을 빌드해서 수동 테스트를 돌리면서 버그를 찾아내야 하기 때문에 디버깅 시간도 늘어나고 매번 QA 시간에서 고통받게 될 것이다..

 

또한 모든 뷰와 기능에 대해서 전체 테스트 코드를 짤 필요도 없다!

 

다만 테스트 코드에 대한 이해가 있는 개발자와, 없는 개발자가 같은 프로젝트를 시작했을 때 코드 결과물이라던가 생산량이 다르다고 생각한다.

전자는 버그가 생기거나 이슈가 생겼을 때 해당 버그를 재현하는 테스트 케이스를 작성해서 그 테스트 케이스만 통과하도록 로직을 수정하면 된다. 그럼 연관되어 있는 다른 버그가 터질 일도 없다.

반면 후자는 버그를 해결하기 위해서 계속 앱을 돌리면서 로그를 확인하고 브레이크 포인트를 찍어가며 추적하는 방식이다. 눈에 보이는 버그가 해결됐다면(더 이상 xcode에 빨간 불이 안 뜬다면) 해결됐다고 생각할 것이다.

 

따라서 본인의 프로젝트 규모와 현재 상황에 맞게 테스트 코드를 사용하는 것이 중요하지만, 알고 있어서 나쁠 건 없다고 생각한다!

 

 

 

📍  UITest

UITest의 목적은 주로 사용자 인터페이스 부분을 테스트하기 위함이다. 

버튼이 제대로 눌리는지, 실제 시뮬레이터나 디바이스에서 동작하는지, 네비게이션은 의도한 대로 잘 흘러가는지를 테스트할 수 있다.

우리가 cmd+R을 눌러서 시뮬레이터를 돌리고 하나하나 테스트해보듯이 실제 UITest에서도 이런 동작을 코드로 하게 끔 하는 구조이다.

 

 

Apple에서 XCTest와 같이 결합해서 사용자 인터렉션 flow를 검증하고, UI와 함께 상호작용하는 테스트는 모두 XCUIAutomation을 사용한다고 한다. 

 

 

XCUITest랑 XCUIAutomation이랑 같은 건가요?

 

결과만 말하면 완전 동일한 개념은 아니다.

 

XCUITest는 XCTest에서 UI 테스트만 담당하고 있다. 반면 XCUIAutomation은 UI 테스트를 작성할 때 사용하는 자동화 API를 의미한다. 

 

 

위 레이어로 각 프레임워크의 역할을 이해하면 이해가 더 쉬울 것이다.

 

XCTest는 UnitTest, UITest 등 모든 테스트 프레임워크에 해당한다.

그 하위에 UI Test에 대해서만 자동화하고 검증하는 XCUITest가 존재한다. 그리고 UI 테스트를 작성할 때 UI와 직접 상호작용 할 수 있도록 필요한 함수나, 클래스들을 사용할 수 있도록 하는 것이 XCUIAutomation이다.

 

XCUIAutomation은 따로 설정할 필요 없이 import XCTest를 호출하게 되면 자동으로 XCUIAutomation이 포함되게 된다.

 

import XCTest

final class ScanMateUITests: XCTestCase {

    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.

        // In UI tests it is usually best to stop immediately when a failure occurs.
        continueAfterFailure = false

        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    @MainActor
    func testExample() throws {
        // UI tests must launch the application that they test.
        let app = XCUIApplication()
        app.launch()

        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }

    @MainActor
    func testLaunchPerformance() throws {
        // This measures how long it takes to launch your application.
        measure(metrics: [XCTApplicationLaunchMetric()]) {
            XCUIApplication().launch()
        }
    }
}

 

XCUITest는 Unit Test와 구조가 크게 다르지 않지만, 모든 테스트 함수에 @MainActor가 붙으며 XCUIApplication이라던가 처음 보는 용어들이 꽤 있다. 이게 뭔지 뜯어보자!

 

  • continueAfterFailure : 실패가 발생한 후 테스트 메서드가 계속 실행되어야 하는지 여부를 나타내는 프로퍼티
  • XCUIApplication: 테스트 애플리케이션을 실행하고 모니터링하고 종료할 수 있는 proxy
  • XCUIApplicationLaunchMetric: 앱 실행 성능을 측정할 때 쓰는 성능 지표 클래스

continueAfterFailure = false는 테스트에서 실패가 발생하면 테스트 메서드가 바로 실행을 멈췄으면 좋겠다!라는 의미이다.

 

XCUIApplication을 사용하면 launch()나 terminate() 같은 메서드를 사용해서 UI 테스트에서 앱을 실행하고 종료할 수 있다. XCUIApplication은 테스트 코드에서 실제 앱이랑 상호작용하기 위해서는 꼭 필요로 하는 코드임을 알 수 있다.

 

XCUIApplicationLaunchMetric는 앱이 실행되고 나서 실행시간이 얼마나 걸리는지 성능 측정할 때 주로 사용된다. 또한 특정 빌드에서 앱 실행 속도가 느려지지 않았는지를 확인함으로써 앱의 속도와 성능을 테스트하는데 중요한 클래스이다.

 

💡 MainActor란
: SwiftConcurrency에서 메인 스레드에서의 실행을 보장하도록 하는 전역 actor

 

UITest는 UI요소와 상호작용하고 있다.

버튼 이벤트 생성이라던가, 전달,  UI 요소들은 모두 메인스레드에서 처리하고 있거나 메인스레드에서 처리하기를 권장하고 있다.

 

Unit Test 같은 경우 순수 기능 테스트기 때문에 UI 요소와 전혀 관련이 없어서 @MainActor를 따로 사용하지 않지만, UITest는 시스템 레벨에서 실제 UI와 같이 상호작용하기 때문에 메인스레드에서 작업을 해야 하므로 @MainActor가 필요하다.

 

사용방법

 

테스트 코드 작성법은 Unit Test 코드와 크게 다르지 않다. Test bundle을 추가하고 파일에 들어가 보면 Apple에서 친절하게 주석으로 어떻게 해야 할지 다 작성해 준다.

 

    var app: XCUIApplication!

    override func setUpWithError() throws {
        continueAfterFailure = false

        app = XCUIApplication()
        app.launch()
    }

 

테스트 코드가 실행되기 전, 초기화 코드를 실행하는 함수 setUpWithError에다가 전역으로 앱 인스턴스를 저장할 변수를 선언하고 XCUIApplication을 생성해 주면 버튼, 텍스트 필드, 스크롤 등 UI요소에 접근할 수 있게 된다.

 

그리고 app.launch() 코드를 통해 실제로 앱을 실행시켜 테스트를 해달라고 초기화 함수에 설정해 주었다.

이 과정을 통해서 각 테스트 함수가 실행되기 전에 테스트에 실패하면 바로 중단하도록 설정하고, 모든 테스트가 항상 초기상태로 시작할 수 있도록 코드를 정리해 주도록 한다.

 

    @MainActor
    func testConvertToFileButtonInteraction() throws {
        // 파일 변환 버튼을 탭했을 때 포맷 선택 시트가 나타나는지 확인
        let convertButton = app.buttons["Convert to File"]
        XCTAssertTrue(convertButton.exists)
        
        convertButton.tap()
        
        // 액션시트가 나타나는지 확인
        let pdfOption = app.buttons["PDF"]
        let jpegOption = app.buttons["JPEG"]
        let cancelOption = app.buttons["Cancel"]
        
        // 포맷 옵션들이 표시되는지 확인
        XCTAssertTrue(pdfOption.exists || jpegOption.exists || cancelOption.exists, 
                     "Format selection sheet should appear")
        
        // 취소 버튼으로 시트 닫기
        if cancelOption.exists {
            cancelOption.tap()
        }
    }

 

이 정도의 간단한 UITest를 작성해 봤다.

 

testConvertToFileButtonInteraction()의 역할은 Convert to File이라는 버튼을 눌렀을 때 해당 버튼이 존재하는지 확인하고, 버튼을 눌렀을 때 액션시트가 제대로 나타나는지, 액션시트에 해당하는 포맷들이 제대로 표시되는지를 확인하는 함수이다.

 

UnitTest와 동일하게 테스트를 할 때, Test Assertion을 사용해서 검증을 한다.

  • Boolean Assertion : True나 False를 생성하는 조건을 테스트한다. (XCTAssertTrue/XCTAssertFalse)
  • Nil and Non-Nil Assertion : 테스트 조건이 nil인지 Non-nil인지 테스트한다. (XCTAssertNil/XCTAssertNotNil)
  • Equality and Inequality Assertions : 두 값이 같은지 다른지를 테스트한다. (XCTAssertEqual/XCTAssertNotEqual)
  • Compare Value Assertions : 두 값을 비교해서 더 큰지 작은 지를 테스트한다. (XCAssertGreaterThan, XCAssertLessThanOrEqual...)

 

이외에도 공식문서에 들어가면 엄청 많은 테스트 종류로 구분되고 메서드들도 엄청 많다.

 

나는 그중에서도 버튼이 실제로 존재하는지에 대해 True/False를 내뱉는 XCTAssertTrue 메서드를 사용하여 간단한 테스트 동작을 구현해 보았다. 

 

UI 테스트 코드를 작성하면서 계속 반복되는 패턴들을 테스트 코드로 작성하고 있음을 느꼈다. 실제로 UI Test는 이 버튼을 눌렀을 때 예상된 팝업/요소들이 뜨는지, 네비게이션이 의도대로 흘러가는지를 확인하고 있기 때문에 Unit Test 코드보다 훨씬 단순하게 느껴졌다.

 

let pdfOption = app.buttons["PDF"]

 

그리고 지금 코드에서는 PDF이라는 버튼의 라벨을 찾아서 테스트를 하도록 한다. 이 코드의 단점은 디자인의 변경이 발생했을 때 테스트 코드까지 코드를 수정해야 한다는 것이다. 

 

그래서 accessibilityIdentifier()를 사용해서 식별자를 지정하면 변경되는 디자인에도 대응하기 편하고, 테스트 코드와 실제 UI 코드를 완전히 분리할 수 있다.

 

💡 accessibilityIdentifier
: UI에 보이지 않지만 테스트를 위해 고정된 키처럼 사용할 수 있는 접근성 식별자

 

accesibility가 붙으면 voiceover나 접근성과 관련된 코드일 거라고 생각하지만, 요소를 식별하는 문자열이라고 보면 된다.

 

사용자의 화면에 전혀 표시되지 않지만, UI 테스트를 하는 동안에 특정 UI 컴포넌트를 식별하는 데 사용되기 때문에 UITest를 하는데 있어서 딱 개발자를 위한 기능이라고 볼 수 있다!

 

accessibilityIdentifier를 코드에 적용하면 UI코드에서 식별이 필요한 부분에 코드로 추가해줘야 한다.

예를 들어 PDF라는 버튼에 대해서 적용하고 싶을 경우 아래코드처럼 붙여주면 된다.

 

Button("PDF") {
    // action
}
.accessibilityIdentifier("pdfButton")

 

그럼 테스트 코드에서 UI 코드에 pdfButton이라는 문자열 식별자를 지정해 줬으니 아래와 같이 pdfButton이라는 고유한 값으로 사용할 수 있다. 

 

즉 accessibilityIdentifier로 이름을 지정해 주고 그 이름으로 testcode에 작성할 수 있다고 생각하면 된다!

// 예시 1
XCTAssertTrue(app.buttons["pdfButton"].exists)

// 예시 2
let pdfOption = app.buttons["pdfButton"]

 

이 방법은 UI 구조가 조금만 바뀌어도 유지보수가 쉽고 테스트 코드와 UI 코드를 완전히 분리할 수 있기 때문에 코드 간의 결합도를 낮추는 방법이다.

 

마주친 오류

No such module 'XCTest'

 

프로젝트를 처음 생성할 때 테스트 코드를 작성할 일이 없어서 따로 체크를 하지 않고 만들었다가, 따로 공부해 보려고 Target에서 새로운 UI Testing  Bundle을 생성해 주었더니 위와 같은 오류가 떴다.

 

알아보니 프로젝트 target 링크가 제대로 잡혀있지 않은 경우에 이런 오류가 생긴다고 한다.

 

해결방법을 찾다가 공식 developer 포럼에서 이유를 알아냈다.

 

Xcode의 테스트 Bundle은 XCTest.framework를 가져올 수 있도록 관련 검색 경로 빌드 설정이 자동으로 구성되는데 XCTest를 가져와야 하는 라이브러리와 프레임워크는 이러한 검색 경로를 수동으로 설정해야 한다고 한다.

 

해결방법

 

Xcode 11.4 이상부터는 아래와 같이 Target에 포함되어 있는 Enable Testing Search Paths를 전부 YES로 변경해 주면 해결된다!

 

 

 

테스트 코드는 아래 레포지토리에서 더 자세하게 확인할 수 있습니다!

 

ScanMate/ScanMateUITests/ScanMateUITests.swift at main · yanni13/ScanMate

Contribute to yanni13/ScanMate development by creating an account on GitHub.

github.com

 


참고

https://green1229.tistory.com/431

https://developer.apple.com/documentation/uikit/uiaccessibilityidentification/accessibilityidentifier

https://developer.apple.com/forums/thread/649935

https://developer.apple.com/documentation/XCUIAutomation

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/03   »
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
글 보관함