iOS XCTest 코드 (1)

2022. 3. 2. 15:25iOS/Swift 문법

먼저 CI / CD, iOS앱의 배포 과정 그리고 툴들에 대해 간략히 알아보자

CI / CD - Continuous Integration 지속적 통합 / Continouous Distribution 지속적 배포

   CI/CD는 애플리케이션 개발 단계를 자동화하여 애플리케이션을 보다 짧은 주기로 고객에게 제공하는 방법입니다. CI/CD의 기본 개념은 지속적인 통합, 지속적인 서비스 제공, 지속적인 배포입니다. 
   특히, CI/CD는 애플리케이션의 통합 및 테스트 단계에서부터 제공 및 배포에 이르는 애플리케이션의 라이프사이클 전체에 걸쳐 지속적인 자동화와 지속적인 모니터링하는 것입니다. 이러한 구축 사례를 일반적으로 "CI/CD 파이프라인"이라 부릅니다.
- Red hat - 

요약하자면

CI는 코드 레벨 테스트 자동화

즉, Build -> Test -> Merge

 

CD는 Repository 배포 자동화

iOS앱의 배포 과정

Archive -> Validate -> Distribution -> Appstore Connect -> TestFlight Deployment -> Appstore Deployment

이런 과정들을 배포할 때 마다 수동으로 매번하기는 쉽지 않을 것이다.  그래서 아래와 같은 툴들이 등장했다.

Tool

fastlane은 비교적 간단하고 개인 프로젝트에 적합하고, bitrise(유료) 실 서비스에서 사용된다고 알고 있다. bitrise는 14일 무료를 제공하니 스터디 용도로는 사용 가능하다.

(출시 프로젝트에 fastlane을 우선 적용해보고 튜토리얼을 작성해봐야겠다.)

 

위의 툴로 아래의 작업들을 자동화할 수 있다.

  • Code Signing
  • Appstore Deployment
  • TestFlight Deployment
  • Automatic Screenshots

소개는 여기서 마무리하고 CI의 테스트에 대해서 알아보자

Testing Pyramid

WWDC 19: Test Pyramid

이 피라미드가 나타내는 것은 Testing fundamentals으로 코드 작성의 양의 척도를 나타낸다.

가장 많이 작성해야할 코드는 Unit > Integration > User Interface 순

테스트 비용은 User Interface > Integration > Unit 순

  • Unit: 기능 별로 테스트
  • Integration: 여러 Unit들을 한 번에 테스트
  • User interface: User perspective와 Workflow를 테스트, 테스트하기 힘듦

Integration 테스트를 하는 이유는 같은 기능이라도 동시에 처리하면 정상 작동이 안 되거나 기대하는 결과가 나오지 않을 수 있다.

ㅋㅋㅋㅋ Integration Test 해야하는 예시

Test 

XCTest 프레임워크를 import하고 Abstract XCTest를 구체화한 XCTestCase로 작성한다.

API의 종류로는 XCUIElement / XCUIApplication / XCUIElementQuery 3가지가 있다.

Include Tests 체크

프로젝트를 만들면 TARGET이 3개가 만들어진다. {프로젝트} / {프로젝트}Tests / {프로젝트}UITests 

 

줄번호 옆의 마름모를 통해서 테스트 케이스를 실행하고, UITest의 경우 좌하단의 🔴버튼을 눌러서 테스트 케이스를 작성할 수도 있다.

UITest

import XCTest

class UserTextFieldTest: XCTestCase {
    // 테스트 케이스 실행 전 세팅 메소드로 테스트 케이스 각각 매번 실행
    // 초기화 코드 작성
    override func setUpWithError() throws {
    	// 에러 발생하면 종료
        continueAfterFailure = false
    }
    // 종료 후 메소드로 메모리 해제 등의 구문 작성, 마찬가지로 케이스 각각 실행
    override func tearDownWithError() throws {

	}
    // 테스트명은 최대한 구체적으로 작성 한글로도 작성함
    // snake_case가 쓰이기도 함
    // test메소드는 prefix를 test로 해야 테스트 케이스로 작동함
    func testLoginButtonClick() throws {
        let app = XCUIApplication()
        app.launch()

        // UI Control의 Accesibility의 Identiifer에서 지정
        app.textFields["firstTextField"].tap()
        app.textFields["firstTextField"].typeText("안녕하세요")
		
        // Label text로 접근할 수도 있고, AccesibleIdentifier로 접근할 수도 있다.
        app.buttons["firstButton"].tap()
        // app.staticTexts["First"].tap()
        print(app.textFields["firstTextField"])

        // Assert 구문으로 알려주는 것이 좋은 테스트 코드
        XCTAssertEqual(app.staticTexts.element(matching: .any, identifier: "resultLabel").label, app.textFields["firstTextField"].value as! String, "어떤 것이 잘못되었고, 어떤 걸 수정해야한다. 어떤 메서드와 연관이 있다.")
    }
}
  • setUpWithError(): 테스트 케이스당 매번 실행되며, 초기화를 코드를 여기서 작성해주며 생성자 역할을 한다.
  • tearDownWithError(): 똑같이 매번 실행되며, 메모리 해제 등 소멸자 역할을 한다.
  • Element에 접근하기 위해서 2가지 방법이 있다.
    • app.{UI Control}[ AccessibleIdentifier ]
    • app.staticTexts[ Label text]
  • 마지막에 XCAssert 메소드로 테스트 결과를 검증하는 것이 좋은 테스트 코드이다.

UnitTest

@testable import {프로젝트}: 접근제어자(private, internal)와 상관 없이 접근 가능하게 해주는 키워드

테스트 코드에서 뷰컨트롤러에 접근하여 값을 검증하는 테스트를 만들어 보자!

 import XCTest
 @testable import TestExample

 // system under test or target
    var sut: LoginViewController!

    override func setUpWithError() throws {
        sut = (UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LoginViewController") as! LoginViewController)
        // 뷰를 불러오기 위한 메서드
        sut.loadViewIfNeeded()
    }
    override func tearDownWithError() throws {
        sut = nil
    }
    func testLoginViewController_ValidID_ReturnTrue() throws {
        // Given / Arrange
        sut.idTextField.text = "jack@jack.com"
        // When / Act
        let valid = sut.isValidID()
        // Then / Assert
        XCTAssertTrue(valid, "@가 없거나 6글자 미만이라서 안 될 수 있음")
    }
    // 테스트 기대값이 false일 때
    func testLoginViewController_inValidPassword_ReturnFalse() throws {
        // Given
        sut.passwordTextField.text = "1234"
        // When
        let valid = sut.isValidPassword()
        // Then
        XCTAssertFalse(valid, "패스워드 로직 확인")
    }
    func testLoginViewController_idTextField_RetrunNil() throws {
        sut.idTextField = nil
        let value = sut.idTextField
        XCTAssertNil(value, "id 로직 확인")
    }

sut라는 개념은 System Under Test로 테스트 안에서 주체가 되는 인스턴스이다. 위 예제에서는 테스트할 로직이 있는 뷰컨트롤러이다.

위 테스트 코드는 뷰컨트롤러에 종속적이고 후에 뷰컨트롤러가 수정된다면 재사용이 불가능한 테스트가 될 위험이 있다.

따라서, 아래와 같이 뷰컨트롤러가 아니라 책임과 기능을 최대한 분리하여 아래와 같이 Validator라는 class와 User 구조체를 이용하여 테스트를 작성한다.

struct User {
    let email: String
    let password: String
    let check: String
}

final class Validator {
    func isValidID(id: String) -> Bool {
        return id.contains("@") && id.count >= 6
    }
    func isValidPassword(password: String) -> Bool {
        return password.count >= 6 && password.count < 10
    }
    func isEqualPassword(password: String, check: String) -> Bool {
        return password == check
    }
}
import XCTest
@testable import TestExample

class ValidatorTests: XCTestCase {

    // System Under Test
    var sut: Validator!

    override func setUpWithError() throws {
        sut = Validator()
    }

    override func tearDownWithError() throws {
        sut = nil
    }

    func testIDValid_ReturnTrue() throws {

        // Given
        let user = User(email: "jack@jack.com", password: "123456", check: "123456")
        // When
        let valid = sut.isValidID(id: user.email)
        // Then
        XCTAssertTrue(valid, "ID 로직 학인, 6자리 미만인지 체크")
    }
}