개요
UI Test 는 시뮬레이터 상에서 실제 앱을 구동시켜서 가상 컨트롤러를 이용해서 정해진 시나리오 대로 조작 후 해당 시나리오에 대한 결과를expected result와 비교하여 테스트의 성공/실패를 결정한다.
UI Test는 개발자들사이에서는 많이 사용되지 않는 기능이기는 하나 개발 리소스 혹은 QC에 따로 리소스를 투입하기 힘든 스타트업과 같은 회사에서는 릴리즈시 테스트에 들어가는 비용을 줄여줄 수 있다.
다만 UI Test 또한 적지 않은 단점이 존재 하는데 이중 가장 큰것은 UI/UX가 바뀔때마다 테스트 또한 다시 작성하거나 수정을 해야하는 단점이 있다. UI Test는 화면상에 보여지는 글자, 문자열, 버튼, 아이콘 등 현재의 element를 기준으로 작성이 되고 tap, input text, 화면 전환등 시나리오에 맞는 action을 취했을때 해당 element가 결과에 맞게 표시 혹은 전환, 변경되어 있는지를 보고 성공/실패를 판단하기 때문에 UI/UX가 변경이 된다면 그에 맞게 test또한 수정이 되어야 한다.
위와 같은 단점에도 불구하고 UI/UX가 자주 바뀌지 않거나 큰 틀에서 벗어나지 않는 프로젝트의 경우 UI Test는 리소스 비용 적약과 앱의 안정성 및 높은 퀄리티를 유지할 수 있는 매우 유용한 기능이 될 수 있다.
1. podfile 설정
UI Test는 위에서 언급한 것과 같이 시뮬레이터 상에서 앱을 실행 한 다음 앱의 UI들에 action 을 직접적으로 입력해서 동작시키기 때문에 빌드시 앱의 릴리즈 빌드와 동일한 수준의 pod가 필요하다.
아래와 같이 앱에서 사용되는 pod들을 def XXX 문구를 이용해서 define한 다음 UI Test target에도 포함되어 빌드될 수 있도록 한다.
# 사용되는 pod들을 define 해서 일반 빌드와 UI Test빌드 양쪽에서 사용할 수 있도록 한다.
def sharedPod
# Rx
pod 'RxSwift', '~> 4.5.0'
pod 'RxGoogleMaps', :path => './'
# Firebase
pod 'Firebase/Core', '~> 7.10.0'
pod 'Firebase/RemoteConfig', '~> 7.10.0'
pod 'Firebase/Performance', '~> 7.10.0'
pod 'Firebase/Messaging', '~> 7.10.0'
pod 'Firebase/Analytics', '~> 7.10.0'
pod 'Firebase/Crashlytics', '~> 7.10.0'
# AWS
pod 'AWSCore', '~> 2.23.3'
pod 'AWSCognitoIdentityProvider', '~> 2.23.3'
pod 'Amplify', '~> 1.8.0'
pod 'AmplifyPlugins/AWSCognitoAuthPlugin', '~> 1.8.0'
# UI
pod 'SnapKit', '~> 4.2.0'
pod 'MarqueeLabel', '~> 4.0.5'
pod 'EasyTipView', '~> 2.1.0'
pod 'FSPagerView', '~> 0.8.3'
pod 'Kingfisher', '~> 7.0'
# Amplitude
pod 'Amplitude', '~> 8.5.0'
# Misc
pod 'Fabric', '~> 1.10.2'
pod 'Branch', '0.27.1'
pod 'ReachabilitySwift', '4.3.1'
pod 'NMapsMap'
pod 'CryptoSwift', '~> 1.3.8'
pod 'ChannelIOSDK', podspec: 'https://mobile-static.channel.io/ios/9.1.2/xcframework.podspec' # 채널톡
end
target 'UITESTExample' do
# 위에서 define 한 항목을 정의
sharedPod
target 'UITESTExampleTests' do
inherit! :search_paths
# Pods for testing
end
target 'UITESTExampleUITests' do
inherit! :search_paths
# Pods for testing
# UI Test에서도 일반 빌드와 동일한 pod를 사용하기 위에서 define 한 항목을 정의
sharedPod
end
end
2. UI Test를 위한 syntax 사용법
swift의 UI component는 모두 accessibilityIdentifier 이라는 member variable을 가지고 있다. 이는 UI Test에서 접근하려고 하는 element 혹은 component 를 찾을때 유용하게 사용되기 때문에 모든 ui component에 고유한 string을 넣어서 관리하는것이 UI Test를 쉽게 작성하는 방법중의 하나이다.
아래와 같이 UIImageView에 accessibilityIdentifier를 설정 후 UI Test에서 간편하게 해당 imageView에 접근할 수 있다.
// source code
var guideImage: UIImageView = UIImageView()
guideImage.accessibilityIdentifier = "guide_image" // image view에 접근할 문자열 설정
// UI Test Code
let app = XCUIApplication()
app.images["guide_image"].exists // guide_image 라는 image view에 접근 및 존재 하는지 확인
XCTAssert
시나리오대로 action을 취했을때 화면상에 표시되는 아이콘, 버튼, 이미지, 문구 등이 정상적으로 표시가 되었는지 확인 하고 Test Case의 성공 / 실패를 판단 할때에 사용한다. XCTAssert외에도 XCTFail, XCTAssertTrue 등 여러 method가 많이 있기 때문에 상황에 맞는 method를 사용해서 성공/실패를 판단하면 된다.
// check button이 있는 경우 성공
XCTAssert(app.buttons["check"].exists)
// "TEST" 라는 문자열이 있는 경우 성공
XCTAssert(app.staticTexts["TEST"].exists)
// 2초 내로 "TEST2" 라는 문자열이 보여지는 경우 성공
XCTAssert(app.staticTexts["TEST2"].waitForExistence(timeout: 2))
// 무조건 실패
XCTFail()
// image1 이라는 이미지가 있는 경우 성공, 실패하는 경우 message 출력
XCTAssert(app.images["image1"].exists, "There is no image1 image")
Button Tap / Double Tap
화면상의 "button1" 버튼을 tap 한다.
let app = XCUIApplication()
app.buttons["button1"].tap()
app.buttons["button2"].doubleTap()
NavigationBars
NavigationBars중 "my page" 라는 항목에 접근한다.
let app = XCUIApplication()
app.navigationsBars["my page"]
image 확인
화면 상에 image_name이라는 이미지가 있는지 확인할 수 있다.
app.images["image_name"].exists
화면 상의 특정 위치 tap
아래 예제는 탭 위치는 {X 좌표 : width를 8등분해서 오른쪽에서 1/8 만큼 떨어지 위치, Y 좌표 : 화면의 중간} 위치를 가상 컨트롤러가 tap하도록 한다.
let pointX = (app.frame.width/8) * 7
let pointY = app.frame.height/2
let pointLoc = CGVector(dx: pointX, dy: pointY)
let normalizedLoc = app.coordinate(withNormalizedOffset: CGVector(dx:0, dy: 0))
let tapLoc = normalizedLoc.withOffset(pointLoc)
tapLoc.tap()
화면상의 이미지 확인
UITest에서 특정 image를 확인하기 위해서는 UIImageView의 accessibilityIdentifier에 assign되어 있는 name(일반적으로 resource name을 사용)을 이용해서 찾을 수 있다.
우선 아래와 같이 UIImageView의 accessibilityIdentifier에 찾을 이미지의 특정 name을 넣고
// 단일 이미지의 경우
let imageView:UIImageView = UIImage(named: "resource_name")
imageView.accessibilityIdentifier = "resource_name"
// 여러 image를 상황에 따라 하나의 imageView에 보여주는 경우
var imageName:String = "default_resource_name"
switch index {
case 0:
imageName = "resource_name_1"
case 1:
imageName = "resource_name_2"
case 2:
imageName = "resource_name_3"
case 3:
imageName = "resource_name_4"
default:
print("use default")
}
imageView.image = UIImage(named: imageName)
imageView.accessibilityIdentifier = imageName
아래와 같이 UI Test에서 해당 image가 현재 존재 하는지 확인할 수 있다.
let app = XCUIApplication()
XCTAssert(app.images["image_name"].exists, "There is no image_name")
TextField 의 입력값 가져오기
TextField에 입력된 값을 비교 해야 하는 경우 아래와 같은 방법으로 해당 입력 값을 가져온 다음 결과 값을 비교해서 유효성을 테스트 할 수 있다.
let value = app.textFields["TextFieldName"].value as! String
XCTassertEqual(value, "Expected String")
예제
위에서 설명한 기본 기능들을 이용해서 아래와 같은 테스트 코드를 작성할 수 있다.
아래 예제는
- "rideStart" 버튼을 클릭
- 사용자 이름을 입력하는 화면이 전환되면 해당 화면의 버튼, textField, image들을 확인해서 화면전환 성공 여부 확인 확인
- username을 자동으로 입력하고 인증 번호 요청 버튼을 클릭
- 인증 번호 입력 화면의 text, textField, button을 확인해서 화면 전환의 성공 여부 확인
- 인증 번호 자동입력
- 확인 버튼 클릭
- 로그인이 완료되어 메인 버튼의 text가 변경된 것을 확인 후 성공 여부를 확인
의 순서로 진행되며 탭, 입력등을 자동화 하고 화면이 전환 될때마다 해당 화면이 정상적으로 전환이되었는지 element들을 확인하고 있다.
import XCTest
class UITestLogin: 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
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
XCUIApplication().launch()
// 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.
}
func testExample() throws {
// Use recording to get started writing UI tests.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testlogin() throws {
let app = XCUIApplication()
guard app.buttons["rideStart"].exists else {
XCTFail()
return
}
if !app.buttons.staticTexts["로그인하기"].waitForExistence(timeout: 1) {
// do logout
}
app.buttons["rideStart"].tap()
XCTAssert(app.staticTexts["휴대폰 번호를 입력해 주세요."].exists)
XCTAssert(app.textFields["usernameTextField"].exists)
XCTAssert(app.buttons["signinViewAgreeCheck"].exists)
XCTAssert(app.buttons["signinViewSigninButton"].exists)
XCTAssert(app.buttons["icCheckBoxDefault"].exists)
app.textFields["usernameTextField"].tap()
app.textFields["usernameTextField"].replaceText(newText: "010-1111-2222")
app.buttons["signinViewAgreeCheck"].tap()
XCTAssert(app.buttons["icCheckBox"].waitForExistence(timeout: 2))
app.buttons["signinViewSigninButton"].tap()
XCTAssert(app.staticTexts["인증번호를 입력해 주세요."].waitForExistence(timeout: 2))
XCTAssert(app.textFields["confirmCodeTextField"].exists)
XCTAssert(app.buttons["signinViewConfirmButton"].exists)
sleep(3) // wait for sending confirm code
app.textFields["confirmCodeTextField"].typeText("777777")
sleep(1) // wait for activating confirm button
app.buttons["signinViewConfirmButton"].tap()
XCTAssert(app.buttons.staticTexts["이용하기"].waitForExistence(timeout: 5))
}
func testLogout() throws {
}
}
extension XCUIElement {
func clearText(andReplaceWith newText:String? = nil) {
//When there is some text, its parts can be selected on the first tap, the second tap clears the selection
tap()
tap()
press(forDuration: 1.0)
let selectAll = XCUIApplication().menuItems["Select All"]
//For empty fields there will be no "Select All", so we need to check
if selectAll.waitForExistence(timeout: 0.5), selectAll.exists {
selectAll.tap()
typeText(String(XCUIKeyboardKey.delete.rawValue))
}
if let newVal = newText { typeText(newVal) }
}
func replaceText(newText: String) {
if let existedText = value as? String, !existedText.isEmpty {
doubleTap()
}
typeText(newText)
}
}
'개발 > ios' 카테고리의 다른 글
[iOS] RxSwift + Reactorkit 을 사용해서 프로젝트를 구성해보자 - Project Template (0) | 2022.02.27 |
---|---|
[iOS] DI(Dependency Inject) with SwInject (0) | 2022.01.28 |