TDD 진짜 너무너무너무너무 어렵다...(다음 프로젝트 기회에ㅎㅎ)
Unit Test 하는 것도 어렵다.............
어려운게 아니라 익숙하지 않은 것이라고 생각하자!
이번 포스팅 주요 내용 = iOS Unit Testing and UI Testing Tutorial
iOS Unit Test와 UI Test의 Tutorial이 있는 곳을 찾았는데, 비동기 작업에 대한 내용도 있어서 이번 프로젝트에 써먹을 수 있을 듯? 하다ㅎ...
오늘은 해당 사이트 내용을 정리(번역)해보자!
우리는 어떤 테스트를 작성하든지, 먼저 우리가 테스트할 필요가 있는지 부터 봐야한다.
일반적으로 테스트는 아래를 포함해야 한다.
F.I.R.S.T는 효과적인 단위 테스트를 위한 간결한 기준을 설명한다
테스트 네비게이터는 테스트 작업을 쉽게 하는 방법들을 제공한다. 이를 사용하여 테스트 대상을 만들고 앱에 대해 테스트를 실행한다.
Newe Unit test Target 을 통해서 Test를 생성하면 Default 로 생성되어 있는 setUpWithError()
, tearDownWithError()
를 볼 수 있다.
테스트는 세 가지 방법을 통해서 실행할 수 있는데,
모든 test class들 실행
Test Navigator에 있는 화살표 버튼을 통해 실행
코드 옆 다이아몬드 버튼을 클릭하여 실행
testPerformanceExample()
과 testExample()
에 대한 설명이 없어 보인다. 따로 찾아보자!먼저, 해당 사이트에서 제공해준 BullsEye 의 모델의 핵심 기능을 XCTAssert
함수를 통해서 테스트 해보자!
@testable import BullsEye
을 추가 함으로써 BullsEye 모듈 내의 타입과 함수들에 접근
var sut: BullsEyeGame!
테스트 클래스 내에 System under Test(SUT)
을 만들어 준다.
try super.setUpWithError()
sut = BullsEyeGame()
그리고 setUpWithError() 함수 내에 위 코드를 추가한다. 이렇게 하면 BullsEyeGame 클래스 수준에서 생성되므로(?) 해당 테스트 클래스 내의 테스트에서 sut 인스턴스의 프로퍼티와 메서드에 접근할 수 있게 된다.
→ setUpWithError 내의 코드는 매번 테스트마다 한 번씩 실행되는 것으로 알고 있다..ㅎ
sut = nil
try super.tearDownWithError()
그리고 잊기 전에 tearDownWithError() 메서드 내에 sut 개체에 nil 을 줌으로써 해제시키자.
이것은 모든 테스트가 깨끗한 상태로 시작되도록 SUT 객체 생성한 것을 이 곳에서 해제시켜준다.
첫 번째 테스트 코드 작성!!
func testScoreIsComputedWhenGuessIsHigherThanTarget() {
// given
let guess = sut.targetValue + 5
// when
sut.check(guess: guess)
// then
XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}
테스트 메서드의 이름은 무조건 항상 test로 시작하고 테스트 대상에 대한 설명이 이후 따라야 한다. 테스트 목적과 내용을 명확하기 위해서 한글로도 많이 메서드명을 짓는 것 같다.
그리고 테스트를 알아보기 쉽게 section을 나누어서 작성한다.
check(guess:)
함수를 호출한다. sut.check(guess: guess)
를 통해서 sut.scoreRoun
의 값이 95이 되었다고 생각되어 해당 값들을 서로 비교해주고 있다. false일 경우에는 "Score computed from guess is wrong"이 출력될 것이다.애플 공식문서에 XCTestAssertions 리스트가 있으니 필요한 것을 가져다 쓰면 좋을 것 같다ㅎㅎ
BullsEyeGame
에 일부로 버그를 내장해뒀다고 하는데, 찾아보는 연습을 해보자!!
func testScoreIsComputedWhenGuessIsLowerThanTarget () {
// 주어진
let guess = sut.targetValue - 5
// 언제
sut.check (guess : guess)
// then
XCTAssertEqual (sut.scoreRound, 95 , "추측에서 계산 된 점수가 잘못되었습니다." )
}
해당 코드를 테스트해보면 당연히 Fail 이 나온다. 왜냐고? 위에 코드랑 똑같은데 +5
→ -5로 했으면 당연히 반대로 결과가 나오겠지??ㅎㅎ
BreakPoint Navigator에서 Test Failure Breakpoint를 설정해보자!
그럼 Test Fail시에 Breakpoint가 걸리면서
해당 값에 대한 자세한 내용을 볼 수 있다.
더 상세하게 보기 위해서는 모델이 정의되어 있는 BullsEyeGame.swift 에 Breakpoint를 걸고 테스트를 실행해보면 된다.
이제 본격적으로 지금 BankManager
프로젝트에서 사용될만한 비동기 작업 테스트에 대해서 알아보자!!
예제 모델인 BullsEyeGame
에서는 URLSession
을 다음 게임의 목표로 난수를 얻는데 사용한다. URLSession
메서드는 비동기적이다. 즉시 반환되지만 나중에 실행이 완료되지 않는다. 비동기 메서드를 테스트하기 위해서는 XCTestExpectation
을 사용해서 비동기 함수가 완전히 끝날때까지 테스트가 리턴되지 않도록 해야된다.
일단 예제를 진행해보자!!
이 클래스의 모든 테스트는 URLSession 을 사용하여 요청을 전송하므로, SUT로 선언하고 객체를 생성하고 테스트가 끝나면 해제하도록 해주자!
var sut: URLSession!
override func setUpWithError() throws {
try super.setUpWithError()
sut = URLSession(configuration: .default)
}
override func tearDownWithError() throws {
sut = nil
try super.tearDownWithError()
}
그리고 테스트 함수를 추가해주자!!
// Asynchronous test: success fast, failure slow
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)!
// 1
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 {
// 2
promise.fulfill()
} else {
XCTFail("Status code: \(statusCode)")
}
}
}
dataTask.resume()
// 3
wait(for: [promise], timeout: 5)
}
해당 테스트를 차근차근 알아가보자! 이 테스트는 유효한 요청을 보내면 statusCode
값으로 200을 보내는지 확인하는 테스트이다.
XCTestsExpectation
을 반환한다. promise
상수에 저장되며, 예상되는 작업에 대해서 기술한다.timeout
간격이 끝날 때까지 테스트를 계속 실행한다.해당 내용 BankManager 프로젝트에 적용
func test_Notification이_오는지_확인() {
let expectation = XCTestExpectation(description: "성공")
NotificationCenter.default.addObserver(self, selector: #selector(success),
name: NSNotification.Name(rawValue: "completedCustomer"),
object: nil)
let customers: [Int: Customer] = [1:Customer(order: 1)]
bankManager.setBankCounters(number: 1)
bankManager.process(customers, completionHandler: {
expectation.fulfill()
})
wait(for: [expectation], timeout: 3)
}
@objc func success() {
XCTAssert(true)
}
To be Continue....
참고 : 비동기 함수에 대한 test 작성에 대한 팁!! How to test asynchronous functions using expectation()
공식 문서 내에 테스트에 대한 내용 : Apple Devloper Document
즉시 반환되지만 나중에 실행이 완료되지 않는다 --> 비동기 함수는 즉시 반환되지만 나중에 실행이 완료됩니다 !