[SwiftUI] UI Testing

Junyoung Park·2022년 8월 23일
0

SwiftUI

목록 보기
46/136
post-thumbnail
post-custom-banner

UI Testing a SwiftUI application in Xcode | Advanced Learning #18

UI Testing

구현 목표

  • Unit 테스트가 비즈니스 로직을 점검하는 방법이라면 UI 테스트는 스위프트 뷰 컴포넌트를 테스트하는 방법
  • 모든 버튼, 클릭, 텍스트필드의 텍스트 화면 등이 기대하는 대로 동작하는지 확인 필요
  • 앱 런치 전 환경변수, 아규먼트로 특정 변수 값을 전달, 온보딩을 제외하고 값을 실행 가능 → UI 테스팅 시 소모되는 시간을 줄일 수 있는 효율적인 방법 → 의존성 주입을 통해 앱 런치 단계에서 불리언 변수를 통해 플래그 비트 전달
  • UI 테스팅 단계에서 공통적으로 사용되는 코드 함수화

테스트 작성 방법

  • 앱 실행 이전 환경변수 또는 아규먼트로 전달하는 edit Scheme
  • 실제 시뮬레이터/디바이스 연결을 통해 앱을 사용 시 활용 가능
    override func setUpWithError() throws {
        continueAfterFailure = false
        app.launchArguments = ["-UITest_startSignedIn"]
        app.launch()
    }
  • UI 테스팅 단계에서 사용할 환경변수는 app.launchArguments를 통해 특정 값을 전달 → UI 테스팅 단계의 각 테스트들이 UITest_startSignedIn을 사용할 때 불필요한 크래시가 나지 않는지 확인 필요

소스 코드

import SwiftUI

class UITestingBootCampViewModel: ObservableObject {
    let placeholderText: String = "Add your name..."
    @Published var textFieldText: String = ""
    @Published var currentUserIsSignedIn: Bool
    
    init(currentUserIsSignedIn: Bool) {
        self.currentUserIsSignedIn = currentUserIsSignedIn
    }
    
    func signUpButtonPressed() {
        guard !textFieldText.isEmpty else { return }
        currentUserIsSignedIn = true
    }
}

struct UITestingBootCampView: View {
    @StateObject private var viewModel: UITestingBootCampViewModel
    
    init(currentUserIsSignedIn: Bool) {
        _viewModel = StateObject(wrappedValue: UITestingBootCampViewModel(currentUserIsSignedIn: currentUserIsSignedIn))
    }
    
    var body: some View {
        ZStack {
            LinearGradient(
                gradient: Gradient(colors: [.blue, .cyan, .indigo]),
                startPoint: .topLeading,
                endPoint: .bottomTrailing)
                .ignoresSafeArea()
            
            ZStack {
                if viewModel.currentUserIsSignedIn {
                    SignedInHomeView()
                }
                
                if !viewModel.currentUserIsSignedIn {
                    signUpLayer
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                        .transition(.move(edge: .leading))
                }
            }
        }
    }
}

extension UITestingBootCampView {
    private var signUpLayer: some View {
        VStack {
            TextField(viewModel.placeholderText, text: $viewModel.textFieldText)
                .accessibilityIdentifier("SignUpTextField")
                .font(.headline)
                .padding()
                .background(.white)
                .cornerRadius(10)
            Button {
                withAnimation(.spring()) {
                    viewModel.signUpButtonPressed()
                }
            } label: {
                Text("Sign Up")
                    .accessibilityIdentifier("SignUpButton")
                    .font(.headline)
                    .withDefaultButtonFormmating(Color.pink.opacity(0.6))
                    .padding(.horizontal, 60)
            }
            .withPressableStyle(0.9)
        }
        .padding(.horizontal, 30)
    }
}

struct SignedInHomeView: View {
    @State private var showAlert: Bool = false
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Button {
                    showAlert.toggle()
                } label: {
                    Text("Show Welcome Alert!")
                }
                .accessibilityIdentifier("ShowAlertButton")
                .alert(isPresented: $showAlert) {
                    Alert(title: Text("WELCOME TO THE WORLD!"))
                }
                NavigationLink {
                    Text("DESTINATION")
                        .font(.headline)
                        .padding(.horizontal, 60)
                } label: {
                    Text("NAVIGATION")
                        .font(.headline)
                        .withDefaultButtonFormmating(Color.pink.opacity(0.6))
                        .padding(.horizontal, 60)
                }
                .accessibilityIdentifier("NavigationLinkToDestination")
            }
            .padding()
            .navigationTitle("WELCOME")
        }
    }
}
  • UI 테스팅을 진행하기 위해 테스트 용으로 만든 UI
  • 텍스트를 입력했다면 온보딩 뷰에서 홈뷰로 이동, 네비게이션 이동 가능
  • 앱 런치 단계에서 로그인 여부(로그인 화면에서 바로 홈뷰로 넘어갈 것인지 결정)를 결정하기 위한 불리언 변수 의존성 주입
import SwiftUI

@main
struct SwiftfulThinkingAdvancedLearningApp: App {
    let currentUserIsSignedIn: Bool
    init() {
        // First Initializer when app launched
         let userIsSignedIn: Bool = CommandLine.arguments.contains("-UITest_startSignedIn") ? true : false
//        let userIsSignedIn: Bool = ProcessInfo.processInfo.environment["-UITest_startSignedIn2"] == "true" ? true : false
        self.currentUserIsSignedIn = userIsSignedIn
    }
    var body: some Scene {
        WindowGroup {
            UITestingBootCampView(currentUserIsSignedIn: currentUserIsSignedIn)
        }
    }
}
  • userIsSignedIn이라는 환경변수를 앱 실행 이전 입력 가능
extension UITestingBootCampView_UITests {
    func signUpAndSignIn(shouldTypeOnKeyboard: Bool) {
        let textField = app.textFields["SignUpTextField"]
        textField.tap()
        
        if shouldTypeOnKeyboard {
            textField.typeText("ID")
        }
        let signUpButton = app.buttons["SignUpButton"]
        signUpButton.tap()
    }
    
    func tapAlertButton(shouldDismissButton: Bool) {
        let showAlertButton = app.buttons["ShowAlertButton"]
        showAlertButton.tap()
        if shouldDismissButton {
            let alert = app.alerts.firstMatch
            let alertOKButton = alert.scrollViews.otherElements.buttons["OK"]
            alertOKButton.tap()
        }
    }
    
    func tapNaivgationLink(shouldDismissDestination: Bool) {
        let navigationLinkButton = app.buttons["NavigationLinkToDestination"]
        navigationLinkButton.tap()
        if shouldDismissDestination {
            let backButton = app.navigationBars.buttons["WELCOME"]
            backButton.tap()
        }
    }
}
  • UI 테스팅 함수에서 공통적으로 사용할 함수 → 텍스트 입력 창에 텍스트를 누를 것인지, 네비게이션 버튼을 누를 것인지 등 불리언 변수를 통해 양식 조절 가능
  • 불필요한 코드를 줄이는 효율적인 함수화 기법
    func test_UITestingBootCampView_singUpButton_shouldNotSignIn() {
        // Given
        signUpAndSignIn(shouldTypeOnKeyboard: false)
        // When
        let navBar = app.navigationBars["WELCOME"]
                
        // Then
        XCTAssertFalse(navBar.exists)
    }
    
    func test_UITestingBootCampView_singUpButton_shouldSignIn() {
        // Given
        signUpAndSignIn(shouldTypeOnKeyboard: true)
        // When
        let navBar = app.navigationBars["WELCOME"]
                
        // Then
        XCTAssertTrue(navBar.exists)
    }
  • 키보드에 타이핑을 할 것인지 여부만을 불리언 변수를 통해 비교하게 되면, 기존 UI 설계에서 타이핑 여부에 따라 홈뷰 이동을 결정하는 로직만을 검사할 수 있음

네비게이션, 알러트 등 다른 UI 컴포넌트는 UI 테스팅 코드를 통해 조작이 쉬웠는데, 시뮬레이터 HW 연결 시 키보드 입력 이슈가 있어서 시간이 좀 걸렸다. 결과적으로 직접 특정 버튼을 타이핑해서 텍스트 필드에 텍스트를 입력하는게 아니라, textField.typeText 메소드를 통해 곧바로 텍스트를 넣을 수 있었다.

구현 화면

  • 테스트 클래스를 모두 실행한 모습
profile
JUST DO IT
post-custom-banner

0개의 댓글