2022. 3. 2. 15:25ㆍiOS/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
이 피라미드가 나타내는 것은 Testing fundamentals으로 코드 작성의 양의 척도를 나타낸다.
가장 많이 작성해야할 코드는 Unit > Integration > User Interface 순
테스트 비용은 User Interface > Integration > Unit 순
- Unit: 기능 별로 테스트
- Integration: 여러 Unit들을 한 번에 테스트
- User interface: User perspective와 Workflow를 테스트, 테스트하기 힘듦
Integration 테스트를 하는 이유는 같은 기능이라도 동시에 처리하면 정상 작동이 안 되거나 기대하는 결과가 나오지 않을 수 있다.
Test
XCTest 프레임워크를 import하고 Abstract XCTest를 구체화한 XCTestCase로 작성한다.
API의 종류로는 XCUIElement / XCUIApplication / XCUIElementQuery 3가지가 있다.
프로젝트를 만들면 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자리 미만인지 체크")
}
}
'iOS > Swift 문법' 카테고리의 다른 글
Using Existential and Generic feat.Dispatch (1) | 2024.09.17 |
---|---|
복잡한 JSON 디코딩하기 - NestedContainer, custom init (0) | 2024.09.17 |
면접 질문: Closure에서 weak self를 사용하지 않아도 되는 경우? (0) | 2024.09.08 |
Map과 FlatMap, CompactMap (0) | 2022.03.21 |
Class function과 Static function (0) | 2022.02.28 |