Quick
- BDD(Behavior-Driven Development)를 위한 프레임워크
- 각 클로져 단위 마다 descirption을 묶어서 사용하기 때문에 가독성이 좋음
- Given(준비) - When(실행) - then(검증)
- descibe(Given) → context(When) → it(Then) 순으로 구성
- beforeEach 초기화 블록을 통해 it 이 실행 되기 이전의 클로져에 있는 beforeEach 블록들을 순차적으로 실행함
- afterEach 를 통해 it 블록이 실행 된 이후 해야할 초기화 작업을 할 수 있음.
- beforeEach 를 통해 각 it 테스트는 ‘순수한' 상태에서 시작하는 것이 좋음
- 각 it를 테스트 하고 ViewModel의 초기화, 동작이 진행된 코드를 다시 사용해서 테스트 하는 것은 좋지 않다는 뜻.
- 따라서 context 클로져 내부에 context, describe 등이 추가될 수 있음. (동작을 연결 시킨다.)
- ‘동작', ‘초기화' 작업은 beforeEach를 통해서 하자.
- afterEach에서는 메모리 초기화.
- 각 it를 테스트 하고 ViewModel의 초기화, 동작이 진행된 코드를 다시 사용해서 테스트 하는 것은 좋지 않다는 뜻.
Quick 예제
import Quick
class ViewModelTest: QuickSpec {
override func spec() {
//모든 테스트 코드가 spec()을 override 한 함수 내에서 작성됨.
describe("어떤 화면이 로드되고") {
beforeEach {
//...
}
afterEach {
//...
}
context("버튼을 누를 경우") {
beforeEach {
//...
}
it("특정 값이 된다.") {
//...
}
}
}
}
}
Nimble
- XCTAssert 보다 Test 코드를 간단하게 작성할 수 있게 나온 프레임워크
- XCTAssert는 직접 테스트 코드를 작성할 때 실패 메세지를 추가해줘야한다. 그에 반해 Nimble은 실패 메세지를 읽기 쉽게 자동 작성해줌.
- 정말 다양한 assertion 제공
- https://github.com/Quick/Nimble (참고)
Nimble + Quick 예제
//https://github.com/Quick/Quick/blob/main/Documentation/ko-kr/QuickExamplesAndGroups.md
import Quick
import Nimble
class DolphinSpec: QuickSpec {
override func spec() {
describe("a dolphin") {
var dolphin: Dolphin?
beforeEach {
dolphin = Dolphin()
}
describe("its click") {
var click: Click?
beforeEach {
click = dolphin!.click()
}
it("is loud") {
expect(click!.isLoud).to(beTruthy())
}
it("has a high frequency") {
expect(click!.hasHighFrequency).to(beTruthy())
}
}
}
}
}
RxNimble
- RxBlocking, RxTest, Nimble에 의존성이 있다.
expect(self.output.value)
.events(scheduler: scheduler, disposeBag: disposeBag)
.to(equal(
[
.next(0, 0),
...
...
]
))
- Nimble의 expect().to() 에서 expect().events().to()로 events를 체이닝 할 수 있다.
- scheduler에 등록된 가상의 시간과 이벤트에 맞춰 self.output.value의 스트림에 가상시간과 값을 함께 비교할 수 있다.
- TestScheduler 라는 RxTest 라이브러리의 객체가 가상시간을 구현한다.
- 이때 scheduler는 TestScheduler
RxNimble + Quick ViewModel Test 예제
import Foundation
import RxSwift
import RxCocoa
struct TestInput {
var inputTitle: AnyObserver<String?>
var upButton: AnyObserver<Void?>
var downButton: AnyObserver<Void?>
}
struct TestOutput {
var title: Driver<String>
var number: Driver<Int>
}
struct TestViewModel {
let testInput: TestInput
let testOutput: TestOutput
private var disposedBag: DisposeBag = DisposeBag()
//MARK: - Input
private let _inputTitle = BehaviorSubject<String?>(value: nil)
private let _upButton = BehaviorSubject<Void?>(value: nil)
private let _downButton = BehaviorSubject<Void?>(value: nil)
//MARK: - Output
private let _title = BehaviorSubject<String>(value: "")
private let _number = BehaviorRelay<Int>(value: 0)
init() {
testInput = TestInput(
inputTitle: _inputTitle.asObserver(),
upButton: _upButton.asObserver(),
downButton: _downButton.asObserver()
)
testOutput = TestOutput(
title: _title.asDriver(onErrorJustReturn: ""),
number: _number.asDriver(onErrorJustReturn: 0)
)
self._bindInputTitle()
self._bindUpButton()
self._bindDownButton()
}
private func _bindInputTitle() {
self._inputTitle
.compactMap { $0 }
.bind(to: _title)
.disposed(by: disposedBag)
}
private func _bindUpButton() {
self._upButton
.compactMap { $0 }
.map { _ -> Int in
let number = _number.value
return number + 1
}
.bind(to: _number)
.disposed(by: disposedBag)
}
private func _bindDownButton() {
self._downButton
.compactMap { $0 }
.map { _ -> Int in
let number = _number.value
return number - 1
}
.bind(to: _number)
.disposed(by: disposedBag)
}
}
import Foundation
import RxSwift
import RxTest
import Nimble
import RxNimble
import Quick
class RxNimbleTest: QuickSpec {
override func spec() {
var scheduler: TestScheduler!
var dispoedBag: DisposeBag!
describe("어떤 화면이 로드 되고") {
var viewModel: TestViewModel!
beforeEach {
scheduler = TestScheduler(initialClock: 0)
dispoedBag = DisposeBag()
viewModel = TestViewModel()
}
afterEach {
scheduler = nil
dispoedBag = nil
viewModel = nil
}
context("타이틀이 입력 되면") {
beforeEach {
scheduler.createColdObservable([
.next(5, "테스트 타이틀")
])
.bind(to: viewModel.testInput.inputTitle)
.disposed(by: dispoedBag)
}
it("타이틀이 테스트 타이틀로 변경 된다.") {
expect(viewModel.testOutput.title.compactMap { $0 })
.events(scheduler: scheduler, disposeBag: dispoedBag)
.to(equal([
.next(0, ""),
.next(5, "테스트 타이틀")
]))
}
}
context("업 버튼이 3번 터치 되면") {
beforeEach {
scheduler.createColdObservable([
.next(5, Void()),
.next(10, Void()),
.next(15, Void())
])
.bind(to: viewModel.testInput.upButton)
.disposed(by: dispoedBag)
}
it("숫자가 3이 된다.") {
expect(viewModel.testOutput.number)
.events(scheduler: scheduler, disposeBag: dispoedBag)
.to(equal([
.next(0, 0),
.next(5, 1),
.next(10, 2),
.next(15, 3)
]))
}
}
context("다운 버튼이 2번 터치 되면") {
beforeEach {
scheduler.createColdObservable([
.next(5, Void()),
.next(10, Void())
])
.bind(to: viewModel.testInput.downButton)
.disposed(by: dispoedBag)
}
it("숫자가 -2이 된다.") {
expect(viewModel.testOutput.number)
.events(scheduler: scheduler, disposeBag: dispoedBag)
.to(equal([
.next(0, 0),
.next(5, -1),
.next(10, -2)
]))
}
context("업 버튼이 1번 터치 되면") {
beforeEach {
scheduler.createColdObservable([
.next(15, Void())
])
.bind(to: viewModel.testInput.upButton)
.disposed(by: dispoedBag)
}
it("숫자가 -1이 된다.") {
expect(viewModel.testOutput.number)
.events(scheduler: scheduler, disposeBag: dispoedBag)
.to(equal([
.next(0, 0),
.next(5, -1),
.next(10, -2),
.next(15, -1)
]))
}
}
}
}
}
}
'iOS > 학습' 카테고리의 다른 글
Storyboard 에서 Generic Type 의존성 주입하기 2 (0) | 2022.08.29 |
---|---|
Custom Horizontal FlowLayout (0) | 2022.06.21 |
iOS View 에서의 init 함수들 (0) | 2022.05.08 |
iOS RxDataSource로 Custom Xib CalendarView 만들기 (0) | 2022.05.07 |
iOS에서의 Frame vs Bounds (0) | 2022.04.24 |