일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Java
- Stack
- SWiFT
- OSI
- c언어
- System
- windosw 문자열
- 두근두근 자료구조
- 큐
- 파이썬
- ftz level13
- C
- ftz
- 자료구조
- War Game
- HTML
- LoB
- 암호수학
- PHP
- pwnable.kr
- 스택
- 미로 탐색 알고리즘
- web
- 정렬 알고리즘
- 백준
- level13
- 시간복잡도
- 파일 시스템
- windosws wbcs
- 재귀
- Today
- Total
나의 기록, 현진록
iOS에서의 유닛 테스트(Unit Test)에 대해 알아보자 본문
🙋♂️ 유닛 테스트가 무엇인가?
- 유닛 테스트는 다른 말로 단위 테스트라고도 한다.
- 하나의 함수, 메서드 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트입니다.
🙋♂️ 유닛 테스트, 어떤 점이 좋은건가요?
- 해당 메서드에 대해서 독립적(모듈)으로 테스트하기 때문에 빠른 리팩토링 반영과 테스트를 진행할 수 있다.
- 코드의 확장이나 리팩토링 시에도 안정성을 확보한 채로 신속한 대응이 가능하다.
읽기 어렵고 불안정한 유닛 테스트는 코드 기반을 파괴할 수도...(= 무조건 좋은 것은 아니다.)
🙋♂️ 테스트는 어떻게 이루어지나요?
- 예상값과 결과값의 비교로 이루어진다
func testArraySorting() {
let input = [1, 7, 6, 3, 10]
let expectation = [1, 3, 6, 7, 10]
let result = input.sorted()
XCTAssertEqual(result, expectation)
}
🙋♂️ 유닛 테스트 시 지켜야할 FIRST 원칙
- 좋은 유닛 테스트를 위한 원칙이다.
- 실제 5가지 원칙이 모두 지켜지지 않는 경우가 많다. (Timely는 개발 시간이 오래걸린다는 이유로 특히..)
- 이러한 원칙을 염두하고 코딩을 하도록 노력해보자.
[Fast]
- 테스트는 빠르게 동작할 수 있어야 한다. -> 프로젝트 내에 수 많은 테스트 코드가 느리게 동작한다면 비효율적이다.
- 테스트 코드는 빠르게 확인하고, 수정하고, 반영하는 데에 큰 의미가 있기 떄문에 속도가 느린 테스트는 좋지 않다.
[Independent/Isolated]
- 각각의 테스트는 서로 독립적이며 서로 의존해서는 안 된다.
- 의존성이 높은 코드는 완전한 테스트를 진행하기 어려울 수 있고, 테스트가 실패할 경우 오류를 찾기 힘들다.
[Repeatable]
- 어떤 환경에서든 언제 어디서나 테스트는 같은 결과가 반복되어야 한다.
- 통제가 어려운 부분에 대해서는 테스트를 위한 객체를 만들어주는 방법을 선택하기도 한다.
[Self-Validating]
- 테스트는 Bool을 이용하여 성공/실패에 대해서 스스로 검증이 가능해야 한다.
[Timely]
- 이상적인 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.
- 테스트 코드를 작성하기 전에 실제 코드를 구현한다면 테스트하기 까다롭거나 불가능하도록 코딩하게 될 수도 있다.
<실습>
🙋♂️ XCTest
import XCTest
- XCTest는 유닛(단위) 테스트, 퍼포먼스(성능) 테스트, UI 테스트를 만들고 실행하는 프레임워크이다.
- 테스트를 하기 위해서 XCTest가 임포트 되어야 한다.
🙋♂️ XCTestCase
class StrangeCalculatorTests: XCTestCase {
...
}
- XCTestCase는 추상 클래스인 XCTest의 하위 클래스로, 테스트를 하기 위해서 상속해야하는 가장 기본적인 클래스이다.
- XCTestCase를 상속받은 클래스에서는 test에서 사용되는 다양한 프로퍼티와 메서드를 사용할 수 있다.
🙋♂️ setUpWithError()
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
- 각각의 test case가 실행되기 전마다 호출되어 각 테스트가 모두 같은 상태와 조건에서 실행될 수 있도록 만들어 줄 수 있는 메서드이다.
- 만약 어떤 케이스에 의해서 테스트에 사용되는 값이 변경된다면 다음 테스트에서는 정상적인 테스트가 이루어지기 어렵기 때문에 테스트가 이루어질 때마다 테스트의 상태를 reset 해야 한다.
🙋♂️ tearDownWIthError()
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
- 각각의 test 실행이 끝난 후마다 호출되는 메서드이다.
- 보통 setUpWithError()에서 설정한 값들을 해제할 때 사용된다.
여러 개의 테스트 케이스가 실행되는 경우 아래와 같은 순서로 호출된다.
setUpWithError()
[Test Case1]
tearDownWithError()
setUpWIthError()
[Test Case2]
tearDownWithError()
.
.
.
🙋♂️ setUp()과 tearDown() 메서드와의 차이
- setUp(), setUpWithError(), tearDown() / tearDownWithError()의 차이는 에러를 throw할 수 있느냐에 대한 차이이다.
- xcode 11.4 버전 전후로 제공되는 기본 메소드가 다르다.(현재는 ____WithError()가 기본 제공 메소드)
🙋♂️ testExample()
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
}
- test로 시작하는 메서드들은 우리가 작성해야 할 테스트 케이스가 되는 메서드이다.
- 메서드 네이밍의 시작은 무조건 test로 시작되어야 한다.
- 한국인끼리 작업하는 경우에는 영어보다 한글로 네이밍할 수도 있다.(테스트 코드가 기능을 명세화, 문서화하는 역할도 하기 때문)
🙋♂️ testPerformanceExample()
func testPerformanceExample() throws {
// This is an example of a performance test case.
measure {
// Put the code you want to measure the time of here.
}
}
- 성능 테스트를 위한 메서드이다.
- measure(block:) 메서드를 통해 성능을 측정하게 된다.
<StrangeCalculator> - 실습할 타겟이다.
struct StrangeCalculator {
func addNumbers(of numbers: [Int]) -> Int {
return numbers.reduce(0, +)
}
// for TDD
func addOddNumbers(of numbers: [Int]) -> Int {
return 0
}
func addEvenNumbers(of numbers: [Int]) -> Int {
return 0
}
}
🙋♂️ @testable
import XCTest
@testable import UnitTestSample
class StrangeCalculatorTests: XCTestCase {
...
}
- 먼저 테스트 메서드를 작성하기 전에 @testable import (Target)이라는 코드를 작성해 주어야 한다.
- @testable은 유닛 테스트에서 실제 앱 타깃에 있는 코드들에 접근하기 위한 키워드입니다.
- 테스트하는 동안에 접근 수준 제한이 있는 코드에 접근할 수 있도록 해준다.
🙋♂️ SUT(System Under Test)
final class StangeCalculatorTests: XCTestCase {
var sut: StrangeCalculator!
override func setUpWithError() throws {
try super.setUpWithError()
sut = StrangeCalculator()
}
override func tearDownWithError() throws {
try super.tearDownWithError()
sut = nil
}
}
- SUT는 테스트를 진행할 타입이다.(네이밍을 SUT라고 하지 않아도 된다.)
- setUpWithError에서 sut를 초기화 해주고, tearDownWithError()에서 sut에 nil을 할당해 주고 있다. 즉, 테스트 케이스가 시작되는 처음과 끝에 호출되면서 의도된 테스트 환경을 만들어준다.
- super를 호출하는 이유는 XCTestCase를 상속 받았고, override 해서 사용하기 때문이다.
🙋♂️ 테스트 코드 작성하기
- 테스트는 예상값과 결과값을 비교한다.
func test_addNumber호출시_3_7_23을전달했을때_33을_반환하는가(){
// given
let input = [3,7,23]
// when
let result = sut.addNumbers(of: input)
// then
XCTAssertEqual(result, 33)
}
- XCTAssertEqual은 테스트 결과를 확인하는 함수로서 두 값을 비교한 결과에 따라 테스트 통과/실패가 결정된다.
🙋♂️ given / when / then으로 나누어 작성하는 이유?
- BDD(Behavior Driven Development)라는 테스트 방식이다.
- BDD는 시나리오를 설정하여 예상대로 결과가 나타나는지를 확인한다.
- given : 어떤 상황이 주어진다. (조건설정)
- when : 어떤 코드를 실행한다.
- then : 테스트 결과를 확인한다.
- 꼭 이렇게 해야만 하는 것은 아니다, input/output이나 expectation/result로 작성하기도 한다.
거터(테스트 코드 왼쪽 다이아몬드 버튼)를 눌러 테스트 결과를 확인해보자
🙋♂️ 추가 테스트 작성하기
- 메서드의 동작이 아주 단순하기 때문에 문제가 없음을 쉽게 알 수 있다. 하지만 테스트 코드를 작성할 때 예외 케이스는 없는지 확인해 보아야 한다. ex) 음수, 빈 배열, 아주 많은 혹은 큰 수의 경우
- 어느 정도, 어떤 것을 테스트 해야 하는지는 테스트의 목적이나 사람, 팀에 따라 다르다.
🙋♂️ Code Coverage
- 테스트의 가치를 측정해주는 툴이다. 실제 코드에서 어느 정도의 테스트가 진행되었는지 알 수 있다.
- 아래 3가지에 대해서 확인할 수 있다.
- 실제 테스트에서 어떤 코드가 실행되었는지
- 정확성, 성능에 대해 얼마나 충분히 테스트가 이루어졌는지
- 테스트가 포함하고 있지 않은 코드는 무엇인지
사용법
Product - Scheme - Edit Scheme
Close 버튼 클릭 후 테스트 시작해준다.
Command + U 단축키로 전체 테스트를 실행할 수 있다.
이처럼 테스트 진행도를 확인할 수 있다.
LottoMachine.swift의 진행도는 69.7%이고 뭐가 부족한지 확인할 수 있다.
LottoMachine.swift에 마우스를 대면 우측에 버튼이 생기는데 클릭한다.
코드를 내려보면 우측에 빨간 바가 보이는데 마우스를 대면
코드 블럭에 배경이 씌워진다.
🙋♂️ 비동기 메서드 테스트하기
- makeRandomValue(completionHandler:): 네트워크 통신을 통해 random 값을 얻어오는 메서드입니다.
func makeRandomValue(completionHandler: @escaping () -> Void) {
let urlString = "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=30&count=1"
guard let url = URL(string: urlString) else {
return
}
let task = urlSession.dataTask(with: url) { data, response, error in
guard let response = response as? HTTPURLResponse, (200...399).contains(response.statusCode) else {
return
}
guard let data = data, error == nil else {
return
}
do {
guard let newValue = try JSONDecoder().decode([Int].self, from: data).first else {
return
}
self.randomValue = newValue
completionHandler()
} catch {
return
}
}
task.resume()
}
다음과 같이 테스트 코드를 작성한다.
func test_makeRandomValue호출시_randomValue를_0에서30까지숫자로설정해주는지() {
// given
sut.randomValue = 50 // 기본값이 0~30에 포함되면 무조건 테스트에 통과하므로 범위에서 벗어난 값을 할당
// when
sut.makeRandomValue {
// then
XCTAssertGreaterThanOrEqual(self.sut.randomValue, 0)
XCTAssertLessThanOrEqual(self.sut.randomValue, 30)
}
}
좌측에 테스트가 성공했다는 표시가 보인다.
하지만 makeRandomValue는 비동기 메소드로 사실 테스트 코드는 제대로 동작하고 있지 않다.
expectation(description:), fulfill(), wait(for:timeout:) 세가지의 메서드로 비동기적으로 처리되는 동작이 끝날 때까지 기다릴 수 있습니다.
func test_makeRandomValue호출시_randomValue를_잘설정해주는지() {
// given
let promise = expectation(description: "It makes random value") // expectation
sut.randomValue = 50 // 기본값이 0~30에 포함되면 무조건 테스트에 통과하므로 범위에서 벗어난 값을 할당
// when
sut.makeRandomValue {
// then
XCTAssertGreaterThanOrEqual(self.sut.randomValue, 0)
XCTAssertLessThanOrEqual(self.sut.randomValue, 30)
promise.fulfill() // fulfill
}
wait(for: [promise], timeout: 10) // wait
}
- expectation(description:): 어떤 것이 수행되어야 하는지를 description으로 정해줍니다.
- fulfill(): 정의해둔 expectation이 충족되는 시점에 호출하여 동작을 수행했음을 알립니다.
- wait(for:timeout:): expectation을 배열로 담아 전달하여 배열 속의 expectation이 모두 fulfill 될 때까지 기다립니다. timeout을 설정하여 시간을 제한할 수 있습니다.
🙋♂️ Test Double란?
- 테스트를 진행하기 어려운 경우 이를 대신하여 테스트를 진행할 수 있도록 만들어주는 객체
- 테스트 하는 동안에는 테스트 더블과 실제 객체를 잠깐 바꿔서 테스트를 진행한다.
🙋♂️ Test Double의 역할
- 테스트 대상 코드를 격리한다.
- 테스트 속도를 개선한다.
- 예측 불가능한 실행 요소를 제거한다.
- 특수한 상황을 시뮬레이션한다.
- 감춰진 정보를 얻어낸다.
🙋♂️ Test Double의 종류
- Dummy, Stub, Fake, Spy, Mock 등이 있다.
- 테스트 더블마다 역할이 다르지만 사실 명확한 기준으로 구분해서 사용하진 않는다.
- 명확한 구분선을 찾기보다는 흐름에 따라 이해하는 편이 좋다.
Dummy
- 가장 기본적인 테스트 더블이다.
- 어떤 기능이 구현되어 있지 않은, 단지 인스턴스화된 객체로 사용되기 때문에 Dummy의 메서드는 정삭적으로 동작하지 않는다.
- 객체를 전달하기 위한 목적으로 주로 사용된다.
Stub
- Dummy가 실제로 동작하는 것처럼 만들어 실제 코드를 대신해서 동작해주는 객체이다.
- 테스트가 곤란한 부분의 객체를 도려내어 그 역할을 최소한으로 대신해 줄 만큼만 간단하게 구현되어 있다.
Fake
- Stub보다 구체적으로 동작해서 실제 로직처럼 보이지만 실제 앱의 동작에서는 적합하지 않은 객체를 말한다.
- 로직 자체는 실제 앱의 코드와 비슷하지만 그 동작을 단순화하여 구현한 객체이다.
Spy
- Stub의 역할을 가지면서 호출된 내용에 대한 방법 혹은 과정 등 약간의 정보를 기록하는 객체이다.
- 호출이 되었는지, 몇 번 호출되었는지 등에 대한 정보를 기록한다.
Mock
- 실제 객체와 가장 비슷하게 구현된 수준의 객체라고 할 수 있다. Stub이 상태 기반 테스트(State Base Test)라면 Mock은 행위 기반 테스트(Behavior Base Test)라고 이야기하기도 한다.
- 상태 기반 테스트: 메서드를 호출하고 그 결괏값과 예상 값을 비교하는 식으로 동작하는 테스트
- 행위 기반 테스트: 예상되는 행위들에 대한 시나리오를 만들어 놓고, 시나링오대로 동작했는지에 대한 여부를 확인하는 테스트
🙋♂️ 의존성 주입 (Dependency Injection)
- 하나의 객체가 다른 객체의 의존성을 제공하는 기술이다. DI라고 줄여서 부르기도 한다.
🙋♂️ 의존성이란?
class Car {
var wheel: Wheel = Wheel()
}
class Wheel {
var weight = 10
}
- 어떤 객체가 내부에서 생성하여 가지고 있는 객체를 말한다.
- Car 클래스 내부에서 사용된 Wheel이 의존성이다.
- Car 클래스는 Wheel 클래스에 의존하고 있다.
🙋♂️ 의존성 주입이란?
class Car {
var wheel: Wheel
init(wheel: Wheel) {
self.wheel = wheel
}
}
class Wheel {
var weight = 10
}
let myWheel = Wheel()
let myCar = Car(wheel: myWheel)
- 의존성을 주입해준다는 의미는 내부에서 초기화가 이루어지는 것이 아닌, 외부에서 객체를 생성하여 내부에 주입하는 것이다.
- myWheel을 외부에서 생성하여 Car를 초기화할 때 myWheel을 주입시켜주고 이싿.
- 결합도를 낮추기 위해서 사용된다. -> 리팩토링과 테스트 코드 작성이 쉬워진다.
class UpDownGame {
var randomValue: Int = 0
var tryCount: Int = 0
var urlSession: URLSessionProtocol
init(urlSession: URLSessionProtocol = URLSession.shared) {
self.urlSession = urlSession
}
...
}
- 위 코드에서도 URLSession 타입의 의존성을 주입할 수 있도록 되어 있다.
- 테스트를 진행할 때 Test Double 객체를 셀제 URLSession 자리에 바꿔치기 시키면서 테스트를 진행시키기 위함이다.
다음 주소에서 참고하였습니다.
'iOS' 카테고리의 다른 글
[iOS] AppDelegate에 대해서 (0) | 2023.03.29 |
---|---|
iOS에서 TDD(Test-Driven-Development, 테스트 주도 개발) (0) | 2023.03.21 |
[Swift/iOS] UITextView 세로 가운데 정렬 vertical center (0) | 2022.07.26 |
[Swift/iOS] AVPlayer에 Observer 추가하여 재생시간 탐지하기, UISlider을 이용하여 AVPlayer 영상 시점 이동하기, Control View 숨기기/보이기 (0) | 2022.07.19 |
[iOS/Swift] iOS에서 URL로 이미지에 넣어보기 (0) | 2022.07.02 |