Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- 옵저버패턴
- 템플릿메서드
- Scenedelegate
- 컴파운드패턴
- 디자인패턴
- 이터레이터패턴
- 데코레이터패턴
- 스테이트패턴
- SWIFT
- 팩토리메서드패턴
- unowned
- cocoapods
- 어댑터패턴
- ios
- Lifecycle
- 프록시패턴
- 스트래터지패턴
- 상태패턴
- Mobile
- 컴포지트패턴
- 전략패턴
- 싱글턴패턴
- ViewController
- 파사드패턴
- DispatchQueue
- Xcode
- 커맨드패턴
- 추상팩토리패턴
- WKWebView
- RxSwift
Archives
- Today
- Total
ios dev kangwook.
iOS) Xcode Unit Test 본문
테스트 주도 개발은 중요한 건데 아직까지 Xcode에서 Unit Test를 사용하는 게 익숙치 않아서 한 번 정리하려고 한다.
Sample App : BullsEye
- 랜덤으로 제공되는 숫자를 슬라이더 값으로 맞추는 게임 앱
- 숫자는 api를 통해 받아옴
주요 코드
- 테스트를 위해 의도적으로 틀린 부분이 있음
import Foundation
class BullsEyeGame {
var round = 0
let startValue = 50
var targetValue = 50
var scoreRound = 0
var scoreTotal = 0
var urlSession: URLSessionProtocol = URLSession.shared
init() {
startNewGame()
}
func startNewGame() {
round = 1
scoreTotal = 0
}
func startNewRound(completion: @escaping () -> Void) {
round += 1
scoreRound = 0
getRandomNumber { newTarget in
self.targetValue = newTarget
DispatchQueue.main.async {
completion()
}
}
}
@discardableResult
func check(guess: Int) -> Int {
let difference = guess - targetValue // 기존 코드와 다른 부분
scoreRound = 100 - difference
scoreTotal += scoreRound
return difference
}
func getRandomNumber(completion: @escaping (Int) -> Void) {
guard let url = URL(string: "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1") else {
return
}
let task = urlSession.dataTask(with: url) { data, _, error in
do {
guard let data = data,
error == nil,
let newTarget = try JSONDecoder().decode([Int].self, from: data).first else {
return
}
completion(newTarget)
} catch {
print("Decoding of random numbers failed.")
}
}
task.resume()
}
}
테스트할 대상 파악
- 일반적으로 테스트는 다음을 포함해야 함
- 핵심 기능 : 모델 클래스, 메서드 및 컨트롤러와의 상호 작용
- 가장 일반적인 UI workflow
- 경계 조건
- 버그 수정
테스트 작성
- 테스트 메서드의 이름은 항상 test로 시작
- given - when - then 으로 섹션 지정(AAA와 유사)
func testScoreIsComputedWhenGuessIsHigherThanTarget() {
// given
let guess = sut.targetValue + 5
// when
sut.check(guess: guess)
// then
XCTAssertEqual(sut.scoreRound, 95, "guess로 계산된 점수가 잘못되었습니다.")
}
func testScoreIsComputedWhenGuessIsLowerThanTarget() {
// given
let guess = sut.targetValue - 5
// when
sut.check(guess: guess)
// then
XCTAssertEqual(sut.scoreRound, 95, "guess로 계산된 점수가 잘못되었습니다.")
- testScoreIsComputedWhenGuessIsLowerThanTarget() 테스트를 진행할 때 통과하지 못함
- guess = targetValue - 5 이지만 scoreRound는 95가 아니라 105가 되기 때문
- 이에 대한 디버깅을 하기 위해서는 일반 디버깅 프로세스를 통해 알아낼 수 있음
- check(guess: )함수에서 difference가 음수값이 될 수 있음
// ...
@discardableResult
func check(guess: Int) -> Int {
let difference = guess - targetValue
scoreRound = 100 - difference
scoreTotal += scoreRound
return difference
}
// ...
- difference가 음수가 되기 때문에 점수가 100 - (-5) = 105
- 따라서 difference의 절대값을 사용해야 함
let difference = abs(targetValue - guess)
XCTestExpectation을 사용하여 비동기 작업 테스트
BullsEyeGame은 URLSession을 사용하여 다음 게임의 대상으로 임의의 숫자를 가져옴
URLSession 메서드는 비동기 방식이고 즉시 반환되지만 실행이 완료되지는 않음
비동기 테스트는 일반적인 테스트보다 느리므로 더 빠른 단위 테스트와 별도로 격리해야 함
BullsEyeSlowTests라는 새 단위 테스트 대상 생성
class BullsEyeSlowTests: XCTestCase {
var sut: URLSession!
let networkMonitor = NetworkMonitor.shared
override func setUpWithError() throws {
try super.setUpWithError()
sut = URLSession(configuration: .default)
}
override func tearDownWithError() throws {
sut = nil
try super.tearDownWithError()
}
func testValidApiCallGetsHTTPStatusCode200() throws {
// given
let urlString = "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1"
let url = URL(string: urlString)!
// expectation(description:) : promise에 저장된 XCTestExpectation을 반환
let promise = expectation(description: "Status code: 200")
// when
let dataTask = sut.dataTask(with: url) { _, response, error in
// then
if let error = error {
XCTFail("Error: \(error.localizedDescription)")
return
} else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
if statusCode. == 200 {
// promise.fulfill() : 비동기 메서드 완료 핸들러의 성공 조건 클로저에서 이것을 호출하여 expectation이 충족되었음을 플래그로 표시
promise.fulfill()
} else {
XCTFail("Status code : \(statusCode)")
}
}
}
dataTask.resume()
// wait(for:timeout:) : 모든 expectation이 충족되거나 시간 초과 간격(timeout)이 끝날 때까지 중 먼저 발생하는 시점까지 테스트를 계속 실행
wait(for: [promise], timeout: 5)
}
func testApiCallCompletes() throws {
// 전제 조건이 실패할 때 테스트를 건너 뛰기 위한 장치(ex. 네트워크 연결 없이 실행될 경우)
try XCTSkipUnless(networkMonitor.isReachable, "Network connectivity needed for this test.")
// given
let urlString = "http://www.notexistrandomnumberapi.com/test"
let url = URL(string: urlString)!
let promise = expectation(description: "Completion handler invoked")
var statusCode: Int?
var responseError: Error?
// when
let dataTask = sut.dataTask(with: url) { _, response, error in
statusCode = (response as? HTTPURLResponse)?.statusCode
responseError = error
promise.fulfill()
}
dataTask.resume()
wait(for: [promise], timeout: 5)
// then
XCTAssertNil(responseError)
XCTAssertEqual(statusCode, 200)
}
참조
'iOS' 카테고리의 다른 글
iOS) StoreKit (1) - StoreKit이란? (1) | 2022.10.18 |
---|---|
iOS) Key-Value Observing (0) | 2022.10.11 |
iOS) Sandbox (0) | 2022.10.09 |
iOS) Static / Dynamic Library (0) | 2022.10.04 |
iOS) Architecture Pattern (0) | 2022.09.29 |
Comments