안녕하세요, 이번에는 Unit Test 코드를 작성하는 방법에 대해 알아보겠습니다. 이전부터 워낙 테스트 코드에 대해 많이 들었어서 중요성은 알고 있었지만, 얼핏 들은 세션에서 너무 어렵게 느껴지는 바람에 지레 겁을 먹은 적도 있습니다. (첫인상이 안좋았습니다.) 하지만 테스트 코드를 넘어 TDD와 CI/CD를 위해서라면 꼭 짚고 넘어가야 하는 개념이기 때문에 꼼꼼히 살펴보도록 하겠습니다. 틀린 부분은 댓글로 언제든지 알려주시면 감사하겠습니다!
왜 테스트 코드를 짜야 하는가?
저는 무언갈 공부하기 전에 그것의 쓰임이나 용도, 필요성을 확실히 느껴야 하는 스타일입니다. 도대체 우리는 왜 테스트 코드를 짜야 하고 저는 이걸 왜 배워야 할까요? '테스트' 코드는 말 그대로 무언갈 시험해보기 위한 코드입니다. 어떤 걸 시험해본다는 걸까요? 그리고 왜 꼭 시험해봐야 할까요? 여태껏 제가 진행했던 프로젝트들을 떠올려 보았습니다.
미달이의 핵심 로직은 각 학과의 URL로부터 장학금 정보를 가져오는 것입니다. 저는 이 로직을 성공적으로 작성하기 위해 매번 print문으로 어떤 정보를 가져오는지 찍어보았고, 시뮬레이터를 통해 앱을 매번 구동시키면서 로직이 잘 구현되었는지 일일이 확인했습니다.
제가 킨디에서 맡은 로직은 Firebase로부터 받은 서점 정보를 테이블뷰에 띄우는 것, 유저들이 작성한 게시글을 큐레이션 게시판에 띄우는 것이었습니다. 이 또한 시뮬레이터를 통해 매번 로직이 잘 구현되었는지 확인했습니다.
미달이, 킨디는 규모가 그렇게 크지 않았기 때문에 매번 시뮬레이터를 통해 앱을 처음부터 구동하는 것에 리스크가 크지 않았습니다. 적당히 참고 테스트할 수 있었습니다. (사실 저때는 테스트 코드의 개념을 몰랐기 때문에 비효율적이다는 생각이 들지 않았습니다. 앱 규모가 작기도 했구요.) 그런데 만약 거대한 서비스의 앱에서 아주 작은 기능을 테스트하는 상황이라면 어떻게 해야 할까요? 내가 작성한 글에서 내용과 사진을 수정하는 로직이 잘 반영되는지 확인해야 하는 상황이라면, 매번 로그인을 하고, 게시판에 들어가서, 내가 쓴 글 찾고, 글만 수정해보고, 사진만 수정해보고, 사진을 삭제도 해보고, 글을 다 삭제해보고, 글과 사진을 같이 수정해보고 ... 생각만 해도 굉장히 많은 시간이 걸립니다. 이것은 마치 자동차의 한 부품을 찾는 과정을 위해 매번 자동차를 끝까지 만들어봐야 하는 상황과도 같습니다. 생각만 해도 비효율적이죠? 이런 비효율적인 상황을 방지하기 위해 테스트 코드가 필요합니다.
무엇을 테스트해야 하는가?
이제 테스트 코드의 필요성을 알았습니다. 그다음으로 궁금했던 것은 '모든 코드에 대해서 테스트 코드를 작성해야 하나? 그럼 너무 시간이 오래 걸리지 않나?' 였습니다. 그래서 현업에서는 어떻게 테스트 코드를 작성했는지 궁금했습니다. 팀마다 테스트 코드를 작성하는 대상이 달랐고, 스타일도 다 달랐습니다만 어느 정도 테스트하는 대상의 공통점은 있었습니다.
테스트 코드를 작성하기 전에, 무엇을 테스트할지 파악하는 게 우선이다. 밑의 항목은 일반적으로 고려하는 테스트 대상이다.
- 핵심 기능 : Model, Class, Method 또는 컨트롤러와의 상호 작용
- 메인 UI의 워크 플로우
- 경계값에서의 상황 (Boundary conditions)
- 버그 수정
이런 대상에 테스트 코드를 작성하는 것이고, 로직을 짜기 전에 먼저 테스트 코드를 작성하는 것을 TDD(Test-Driven Development)라고 합니다. TDD의 방법으로 프로그래밍을 하면 시간이 오래 걸릴 것이라는 생각이 들었지만, 오히려 이것이 전체적인 관점으로 봤을 때 더 효율적이라는 의견도 있었습니다. 이 부분에 대해서는 팀원과 의논해서 스타일을 정하는 것이 좋을 것 같다는 생각이 드네요.
FIRST 원칙
테스트 코드를 작성할 때는 지켜야하는 원칙 다섯 가지가 있습니다. 각 원칙의 첫 글자를 따서 FIRST 원칙이라고 합니다.
- Fast : 테스트는 빠르게 실행되어야 합니다.
- Independent/Isolated : 테스트가 서로 상태를 공유하지 않아야 합니다. 각각 개별적으로 이루어져야 합니다.
- Repeatable : 반복할 수 있어야 합니다. 어떤 상황에서든 같은 결과값을 얻어야 합니다.
- Self-validating : 테스트는 완전히 자동화되어야 합니다. 프로그래머의 로그 파일 해석에 의존하지 않고 '통과' 또는 '실패' 중 하나를 출력해야 합니다.
- Timely : 이상적으로는 테스트를 테스트하는 프로덕션 코드를 작성하기 전에 테스트를 작성해야 합니다. 이를 테스트 중심 개발(TDD, Test-Driven Development)이라고 합니다.
XCTest
iOS 환경에서는 어떻게 테스트 코드를 짤 수 있을까요? Swift에서는 테스트 코드 작성을 위해 XCTest라는 프레임워크를 제공합니다. 우리는 XCTest 프레임워크를 통해 Unit Test, UI Test, Performance Test를 할 수 있습니다. 이를 활용하여 우리가 필요한 부분을 테스트할 수 있습니다.
Unit Test의 종류 : 단순 결과 비교, 비동기 테스트, Fake 테스트
그럼 이제 테스트 코드, 특히 Unit Test 코드를 작성하는 방법에 대해 알아보겠습니다.
Unit Test에는 단순 결과를 비교하는 테스트, 비동기 테스트, Fake 테스트 이렇게 3가지의 카테고리가 있습니다. (뇌피셜) 이 세 가지를 순차적으로 알아보겠습니다. 아래의 BullsEye라는 숫자 맞히기 어플을 이용해서 테스트 코드를 작성해보겠습니다.
Unit Test (단순 결과 비교)
이 부분은 쉽게 말해서, 로직이 예상한 값을 올바르게 반환하는지 확인하는 테스트입니다. 이번에 테스트할 부분은 숫자를 맞혔는지 판단하는 로직입니다. 전체 흐름은 사전 세팅 - 테스트 함수 작성 입니다.
사전 세팅 과정에서는 setUpWithError(), tearDownWithError() 메소드를 활용합니다. (withError가 붙는 것은 fail일 경우 error를 던져준다는 의미로, 어떤 이유로 fail되었는지 확인할 수 있습니다.)
XCTest에서 setUp()은 테스트 메소드가 실행되기 전 호출되는 메소드이고, tearDown()은 모든 테스트 메소드(이름이 "test"로 시작하는 메소드)의 실행이 완료된 후에 호출되는 메소드입니다.
특히 tearDown()의 목적은 테스트 중에 생성된 모든 리소스나 객체를 정리하는 것입니다. 이렇게 하면 각 테스트가 격리되고 후속 테스트에 영향을 줄 수 있는 잔여 효과가 발생하지 않습니다.
예를 들어, 테스트 메서드가 객체의 인스턴스를 생성하는 경우 tearDown()을 사용하여 해당 객체의 할당을 해제하고 관련 메모리를 확보할 수 있습니다. 이렇게 하면 객체가 테스트 메서드의 범위를 벗어나 지속되어 다른 테스트를 방해하는 것을 방지할 수 있습니다.
테스트 함수 작성 에서는 테스트하려는 상황을 작성해줍니다. 이때 함수명은 무조건 test로 시작해야 하며, 뒤에는 한글로 작성하는 경우도 많습니다. 테스트 함수는 given, when, then으로 나눠서 작성하기도 합니다. 그리고 이를 BDD(Behavior Driven Development)라고 합니다.
- given
- 테스트하려는 상황 세팅
- 이 섹션에서는 테스트의 전제 조건을 설정합니다. 테스트 대상 시스템 또는 개체의 초기 상태를 설정합니다. 즉, 테스트가 수행될 단계를 설정합니다.
- 예시: 사용자가 로그인하여 앱의 홈페이지에 있다고 가정합니다.
- when
- 테스트하려는 구체적인 동작 서술
- 이 섹션에서는 테스트 중에 발생하는 작업 또는 이벤트를 설명합니다. 테스트의 실제 실행을 나타내며, 테스트 대상을 명확하게 설명하는 방식으로 작성해야 합니다.
- 예시: 사용자가 홈페이지에서 '항목 추가' 버튼을 클릭할 때.
- then
- 테스트 결과 설명. 결과에 대해 어떤 처리를 할 것인지
- 이 섹션에서는 테스트의 예상 결과 또는 결과를 설명합니다. 테스트가 실행될 때 어떤 일이 일어날지 지정하며, 일반적으로 어설션으로 표현됩니다.
- 예시: 그러면 사용자의 카트에 새 품목이 추가되고 그에 따라 카트 총액이 업데이트되어야 합니다.
테스트 결과를 판단하기 위해 XCT의 메소드를 활용합니다. 대표적인 몇 가지만 알아보겠습니다. 이외의 종류는 공식 문서에서 확인하시는 게 좋습니다.
- XCTAssertEqual(expression1, expression2, ...) - 타입이 같은 두 값이 일치하는지 확인
- XCTAssertNil(expression, ...) - 값이 nil인지 확인
- XCTFail(_ message: String) - 테스트를 Fail로 처리하고 종료시킴
- XCTSkipUnless(expression, ...) - 값의 조건이 충족되지 않는다면 테스트는 Fail
아래의 코드를 통해 전체적인 흐름을 이해하실 수 있습니다.
import XCTest
/// 1. 단위 테스트가 BullsEye의 내부 타입과 함수에 액세스할 수 있게 함
@testable import BullsEye
final class BullsEyeTests: XCTestCase {
/// 2. 테스트 대상 시스템(System Under Test, SUT). 말 그대로 BuulsEysTests 클래스의 테스트 대상으로 BullsEyeGame를 지정. (placeholder라고 표현했음)
var sut: BullsEyeGame!
/// 각 테스트 메서드가 실행되기 전에 호출되는 메서드. 테스트에 필요한 오브젝트나 리소스를 설정하는 데 사용.
override func setUpWithError() throws {
/// 3. 테스트 코드를 작성하기 전 기본 세팅 코드를 먼저 작성해준다. 먼저 부모 클래스의 setUpWithError 메소드를 호출하여, 만약 에러를 포착한다면 호출자에게 에러를 전파한다.
try super.setUpWithError()
sut = BullsEyeGame()
}
override func tearDownWithError() throws {
/// 4. 테스트가 끝나고, 테스트에 생성된 모든 리소스나 객체를 정리. 이렇게 하면 각 테스트가 격리되고 후속 테스트에 영향을 줄 수 있는 잔여 효과가 발생하지 않음.
sut = nil
try super.tearDownWithError()
}
/// 5. 테스트할 함수 작성. 이때 given, when, then 3가지로 나눠서 작성
func testScoreIsComputedWhenGuessIsHigherThanTarget() {
// given : 필요한 값 설정
let guess = sut.targetValue + 5
// when : 테스트중인 코드 실행
sut.check(guess: guess)
// then : 결과 (테스트가 실패할 경우 출력되는 메시지와 함께 예상 결과를 어설트) 계산값/맞는값/틀릴 때의 메세지
XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong.")
}
/// 6. 테스트 디버깅 - 내장된 버그 찾기
/// 좌측 breakpoint 네비게이터 -> 좌측 하단에 + 모양 -> Test Failure Breakpoint 클릭
/// 테스트 중 failure가 발생하면 그곳에서 테스트 멈춤
/// 그곳에서 어떤 문제가 발생했는지 파악하고, 해당 코드 수정해서 디버깅하면 됨!
func testScoreIsComputedWhenGuessIsLowerThanTarget() {
// given
let guess = sut.targetValue - 5
// when
sut.check(guess: guess)
// then
XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong.")
}
}
1. 단위 테스트가 BullsEye의 내부 타입과 함수에 액세스할 수 있게 import 해준다.
2. 테스트 대상 시스템(System Under Test, SUT)을 선언한다. 말 그대로 BullsEyeTests 클래스의 테스트 대상으로 BullsEyeGame를 지정한다.
3. 테스트 코드를 작성하기 전 기본 세팅 코드를 먼저 작성해준다. 먼저 부모 클래스의 setUpWithError 메소드를 호출하여, 만약 에러를 포착한다면 호출자에게 에러를 전파한다.
4. 테스트가 끝나고, 테스트에 생성된 모든 리소스나 객체를 정리하는 코드인 tearDownWithError() 메소드를 작성한다. 이렇게 하면 각 테스트가 격리되고 후속 테스트에 영향을 줄 수 있는 잔여 효과가 발생하지 않는다.
5. 테스트할 함수를 작성한다. 이때 given, when, then 3가지로 나눠서 작성
6. 테스트를 디버깅한다. 내장된 버그를 찾는다.
Asynchronous Test (비동기 테스트)
다음은 비동기 테스트입니다. 비동기 테스트는 어떤 상황에서 필요할까요? 말 그래도 비동기 코드를 테스트하는 것을 비동기 테스트라고 합니다. 그렇기 때문에 테스트 또한 비동기적으로 이루어져야 합니다. 이런 상황으로는 흔히 알고 있는 URLSession을 활용한 코드를 테스트하는 경우에 사용됩니다.
이 앱에서는 URLSession을 통해 랜덤 숫자를 뱉어주는 API로부터 value를 받습니다. 이 부분이 비동기적으로 이루어집니다. 저희는 비동기 통신이 잘 이루어지는지 알아보겠습니다. 만약 통신이 비동기적으로 잘 이루어진다면, HTTP의 response의 status code가 200이겠죠? 이것을 확인해보겠습니다. (나중에 이 내용이 테스트 로직으로 들어가면 되겠죠?)
그 전에 비동기 테스트 코드에는 필요한 것이 3가지 있습니다. 이것들을 비동기 테스트 코드에 사용해야 합니다.
- expectation(description:): 어떤 것이 수행되어야 하는지를 description으로 정해줍니다. objects that XCTest uses to handle waiting
- fulfill(): 정의해둔 expectation이 충족되는 시점에 호출하여 동작을 수행했음을 알립니다. tell XCTest to stop waiting.
- wait(for:timeout:): expectation을 배열로 담아 전달하여 배열 속의 expectation이 모두 fulfill 될 때까지 기다립니다. timeout을 설정하여 시간을 제한할 수 있습니다.
아래의 코드를 보며 전체 흐름과 세부 코드의 의미를 이해할 수 있습니다! 아래의 두 테스트 메소드는 다른 방식으로 구현되어있습니다. 차이점을 살펴봅시다! (Fast Fail)
/// 7. 비동기 테스트 코드는 일반적으로 속도가 느리기 때문에 다른 테스트 코드와 분리해주어야 한다. BullsEye를 import해준다.
import XCTest
@testable import BullsEye
final class BullsEyeSlowTests: XCTestCase {
/// 8. 이 클래스의 모든 테스트는 요청을 전송하기 위해 기본 URLSession을 사용하므로, sut를 선언
var sut: URLSession!
/// 12. 전제 조건 실패에 대한 대비 -> 전제 조건 체크 파라미터 도입
/// NetworkMonitor wraps NWPathMonitor, providing a convenient way to check for a network connection.
let networkMonitor = NetworkMonitor.shared
/// 9. 앞에서 했던 것처럼 setUpWithError()와 tearDownWithError() 코드를 먼저 작성해준다.
override func setUpWithError() throws {
try super.setUpWithError()
sut = URLSession(configuration: .default)
}
override func tearDownWithError() throws {
sut = nil
try super.tearDownWithError()
}
/// 10. 비동기 테스트 코드를 작성해준다. 비동기라서 throws로 반환값 전달!
/// 이 테스트는 유효한 요청을 보내면 200 상태 코드가 반환되는지 확인
func testValidApiCallGetsHTTPStatusCode200() throws {
/// 13. networkMonitor 적용
try XCTSkipUnless(
networkMonitor.isReachable,
"networkdMonitor가 Reachable해야만 밑의 테스트를 진행할 수 있습니다. 그렇지 않다면 테스트를 종료합니다.")
// given
let urlString = "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1"
let url = URL(string: urlString)!
// let url = URL(string: "www.naver.com")!
/// 우리가 기대하는 값.
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()
} else {
XCTFail("Status Code : \(statusCode)")
return
}
}
}
/// 위에서 dataTask가 생성됐으니, 이를 시작하라는 코드
/// 작업은 일시 중단된 상태로 생성되므로 resume() 메서드를 사용하여 명시적으로 시작해야 한다.
/// 일단 시작되면 작업은 비동기적으로 실행되며 작업이 완료되면 완료 핸들러가 호출된다.
dataTask.resume()
/// 모든 기대치가 충족되거나 시간 초과 간격이 끝날 때까지, 둘 중 먼저 발생하는 시점까지 테스트를 계속 실행한다.
wait(for: [promise], timeout: 5)
}
/// 11. Failure인 상황에서 빠르게 결과를 받기 위한 코드 작성
/// (위의 코드는 이미 failure임에도 불구하고, 성공 플래그를 던지지 않는 이상 설정한 제한 시간 끝까지 기다려야 했음)
/// 즉, then의 상황을 when 내부에 두는 것이 아니라, when을 빨리 처리해버리기 위해 then을 밖으로 빼버림
/// 즉, 위의 코드에서 fail이 된다면, 시간 초과 때문에 fail이 뜨는 거고, 여기서 fail이 된다면, error나 statusCode가 맞지 않아서 error가 뜨는 것임. 이걸 쓰면 될듯!
func testAPICallCompletes() throws {
/// 13. networkMonitor와 XCTSkipUnless 적용
try XCTSkipUnless(
networkMonitor.isReachable,
"networkdMonitor가 Reachable해야만 밑의 테스트를 진행할 수 있습니다. 그렇지 않다면 테스트를 종료합니다."
)
// given
let urlString = "http://www.randomnumberapi.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 - 주어진 객체나 표헌식이 nil인지? 즉, nil이어야 통과됨. 만약 nil이 아니면 중지!
/// 여기서는 error가 nil이어야 한다는 의미로 사용. 만약, nil이 아니라면 error가 떴다는 의미이므로 바로 중지시킴
XCTAssertNil(responseError)
/// error가 아님을 확인하고, statusCode가 올바른 값인지 확인해준다.
XCTAssertEqual(statusCode, 200)
}
}
Fake 테스트 (Mock, Stub 등)
다음은 Fake 테스트입니다. 이 테스트가 필요한 상황의 예시를 몇 가지 살펴보겠습니다.
- iOS 앱을 빌드할 때 사용자가 카메라 롤에서 사진을 선택할 수 있도록 UIImagePickerController를 사용할 수 있습니다. 이 객체는 시스템에서 제공하는 객체이므로 그 동작이나 구현을 변경할 수 없습니다. UIImagePickerController와 상호 작용하는 테스트를 작성하는 경우 해당 테스트는 시스템 객체의 동작에 따라 달라지며 느리거나 반복할 수 없을 수 있습니다.
- 또 다른 예는 URLSession 객체를 사용하여 앱에서 네트워크 요청을 하는 것입니다. URLSession 객체의 동작은 시스템에 의해 결정되며 네트워크 연결 및 서버 응답 시간과 같은 다양한 요인에 의해 영향을 받을 수 있습니다. 이러한 외부 요인에 의존하는 경우 URLSession과 상호 작용하는 테스트가 느리거나 반복되지 않을 수 있습니다.
- 앱에 웹 콘텐츠를 표시하기 위해 UIWebView 또는 WKWebView와 같은 시스템 제공 개체를 사용할 수도 있습니다. 이러한 객체는 시스템에서 제공되며 표시되는 웹사이트 또는 디바이스의 네트워크 연결에 따라 예측할 수 없는 동작이 발생할 수 있습니다. 이러한 개체와 상호 작용하는 테스트는 웹뷰 또는 표시되는 콘텐츠의 동작에 따라 느리거나 반복되지 않을 수 있습니다.
즉, 사용자가 제어할 수 없는 외부 요인에 따라 달라지므로 테스트의 속도가 느려지거나 반복할 수 없습니다. (-> FIRST의 두 가지 원칙을 위반) 따라서 mock objects와 같은 임의의 객체를 만들어서 테스트해봐야 하는 것이고, 의존성을 가지는 객체가 존재하는 코드를 테스트하는 경우, 가짜 의존성을 가진 객체를 주입하여야 합니다.
여기서는 임의의 숫자를 받는 URLSession을 위해 Fake 테스트를 해보겠습니다.
사전 세팅 과정은 이전과 똑같습니다만, 테스트 로직 내부가 다릅니다. URLSession을 가짜 URLSession으로 대체하는 것입니다. 그러기 위해서 URLSessionStub 클래스를 활용합니다. 이는 임의의 URLSession으로 교체할 수 있습니다. 교체한 뒤 올바른 값을 뱉는지 확인합니다.
// URLSessionStub.swift
import Foundation
typealias DataTaskCompletionHandler = (Data?, URLResponse?, Error?) -> Void
protocol URLSessionProtocol {
func dataTask(
with url: URL,
completionHandler: @escaping DataTaskCompletionHandler
) -> URLSessionDataTask
}
extension URLSession: URLSessionProtocol { }
class URLSessionStub: URLSessionProtocol {
private let stubbedData: Data?
private let stubbedResponse: URLResponse?
private let stubbedError: Error?
public init(data: Data? = nil, response: URLResponse? = nil, error: Error? = nil) {
self.stubbedData = data
self.stubbedResponse = response
self.stubbedError = error
}
public func dataTask(
with url: URL,
completionHandler: @escaping DataTaskCompletionHandler
) -> URLSessionDataTask {
URLSessionDataTaskStub(
stubbedData: stubbedData,
stubbedResponse: stubbedResponse,
stubbedError: stubbedError,
completionHandler: completionHandler
)
}
}
class URLSessionDataTaskStub: URLSessionDataTask {
private let stubbedData: Data?
private let stubbedResponse: URLResponse?
private let stubbedError: Error?
private let completionHandler: DataTaskCompletionHandler?
init(
stubbedData: Data? = nil,
stubbedResponse: URLResponse? = nil,
stubbedError: Error? = nil,
completionHandler: DataTaskCompletionHandler? = nil
) {
self.stubbedData = stubbedData
self.stubbedResponse = stubbedResponse
self.stubbedError = stubbedError
self.completionHandler = completionHandler
}
override func resume() {
completionHandler?(stubbedData, stubbedResponse, stubbedError)
}
}
// 13~15 : Faking Objects and Interations
// 여기서는 URLSession을 테스트하기 위해 Faking Objects를 사용합니다.
/// 13. Test Navigator의 좌측 하단의 + 버튼을 눌러서 "New Unit Test Class" 클릭. BullsEyeFakeTests 생성.
import XCTest
@testable import BullsEye
/// 14. 기본 세팅 - sut 선언, setUpWithError(), tearDownWithError() 코드 작성
final class BullsEyeFakeTests: XCTestCase {
var sut: BullsEyeGame!
override func setUpWithError() throws {
try super.setUpWithError()
sut = BullsEyeGame()
}
override func tearDownWithError() throws {
sut = nil
try super.tearDownWithError()
}
/// 15. test 함수 작성
func testStartNewRoundUsesRandomValueFromApiRequest() {
// given - sut의 URLSession에 fake object를 만들어서 넣음!
// (1)
let urlString = "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1"
let url = URL(string: urlString)!
let stubbedData = "[1]".data(using: .utf8)
let stubbedResponse = HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: nil,
headerFields: nil)
let urlSessionStub = URLSessionStub(
data: stubbedData,
response: stubbedResponse,
error: nil)
sut.urlSession = urlSessionStub
/// 우리가 기대하는 값
let promise = expectation(description: "Value Received")
// when
sut.startNewRound {
// then
// (2)
XCTAssertEqual(self.sut.targetValue, 1)
promise.fulfill()
}
wait(for: [promise], timeout: 5)
}
// (1) 가짜 데이터와 응답을 설정하고 가짜 세션 객체를 생성합니다. 마지막으로 가짜 세션을 sut의 프로퍼티로 앱에 삽입합니다.
// (2) 스텁은 비동기 메서드인 것처럼 가장하기 때문에 여전히 비동기 테스트로 작성해야 합니다. startNewRound(completion:)를 호출하면 대상 값과 스텁된 가짜 번호를 비교하여 가짜 데이터가 파싱되는지 확인합니다.
}
Coverage 확인하기
이렇게 3가지 카테고리의 Unit Test를 알아보았습니다. 그런데, 이런 테스트 코드를 얼마나 작성해야 테스트가 잘 되었다고 말할 수 있을까요? 이를 확인하기 위한 지표로 Coverage를 확인해야 합니다.
XCode에서 Product > Scheme > Edit Scheme의 메뉴로 가면 이 화면이 나옵니다. 여기서 Test 메뉴에서 Code Coverage for 항목을 체크하고 Close를 해줍니다.
그런 다음 다시 테스트 코드를 실행하면 (Cmd + U) Coverage 수치를 확인할 수 있습니다.
보통 Coverage가 50%는 넘어야 한다고 하니, 부지런히 테스트 코드를 적용해봐야겠습니다. 오늘 배운 내용을 바탕으로, 앞으로 TDD하게 개발하는 습관을 들여야겠습니다!
출처
iOS Unit Testing and UI Testing Tutorial
Learn how to add unit tests and UI tests to your iOS apps, and how you can check on your code coverage.
www.kodeco.com
Testing Tips & Tricks - WWDC18 - Videos - Apple Developer
Testing is an essential tool to consistently verify your code works correctly, but often your code has dependencies that are out of your...
developer.apple.com
'iOS > Swift' 카테고리의 다른 글
[iOS - Swift] Networking 테스트 코드 작성하기 (feat. URLSession) (0) | 2023.03.17 |
---|---|
[iOS - Swift] 의존 관계 역전 원칙 (DIP, Dependency Inversion Principle) (0) | 2023.03.15 |
[iOS - Swift] XCTest FAQ (XCTest를 공부하다 생긴 궁금증들) (0) | 2023.03.07 |
[iOS - Swift] static func vs func (0) | 2023.03.06 |
[Swift] Struct, Initializer (구조체와 이니셜라이저) (0) | 2022.05.10 |