-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Ch.16 Testing with RxTest
๐๐ป๐ฏ๐
โ๐ป ์ด ์ฅ์ ๋์น์ง ๋ง๋ผ. ์ฐ๊ตฌ ๊ฒฐ๊ณผ์ ๋ฐ๋ฅด๋ฉด ๊ฐ๋ฐ์๊ฐ ํ
์คํธ ์์ฑ์ ๊ฑด๋๋ฐ๋ ์ด์ ๋ ๋ ๊ฐ์ง๋ค.
- ๋ฒ๊ทธ๊ฐ ์๋ ์ฝ๋๋ฅผ ์ด๋ค.
- ํ ์คํธ ์์ฑ์ ์ฌ๋ฏธ์๋ค.
์ฒซ ๋ฒ์งธ ์ด์ ๊ฐ ๋น์ ์๊ฒ ํด๋น๋๋ค๋ฉด ๋ฐ๋ก ๊ณ ์ฉ๋ ๊ฒ!
๋ ๋ฒ์งธ ์ด์ ์ ๋์ํ๋ฉด RxTest๋ฅผ ๋ณด์!
์ฑ ์ ์ฝ๊ธฐ ์์ํ๊ณ App ํ๋ก์ ํธ์์ RxSwift๋ฅผ ์ฌ์ฉํ๊ณ ์์ํ ๊ฑฐ ์์ฒด๋ก, RxTest์ RxBlocking๋ฅผ ํตํด RxSwift ์ฝ๋์ ๋ํ ํ ์คํธ๋ ์์ฑํ ์ ์์ด์ผ ํ๋ค. ์์ฒด๋ก ํ ์คํธ ์์ฑ์ ์ฝ๊ณ ์ฌ๋ฏธ์๊ฒ ๋ง๋๋ ์ฐ์ํ API๋ฅผ ์ ๊ณตํ๋ค.
์ด๋ฒ ์ฑํฐ์์๋ RxTest ๋ฐ RxBlocking์ ๋ํด ์์๋ณผ ๊ฑฐ๋ค. iOS ์ฑ ํ๋ก์ ํธ์์ ์ฌ๋ฌ RxSwift ์ฐ์ฐ์์ ์์ฐ RxSwift ์ฝ๋๋ฅผ ๋์์ผ๋ก ํ ์คํธ๋ฅผ ์์ฑํ๋ ์์ ๋ฅผ ํฌํจํด์ ๋ง์ด๋ค.
Getting started
์ด๋ฒ ์ฑํฐ์ ์์ ํ๋ก์ ํธ ์ด๋ฆ์ Testing์ด๋ฉฐ, ์ ๋ ฅํ hex ์์ ์ฝ๋์ ๋ํด์ ๊ฐ๋ฅํ, RGB๊ฐ๊ณผ ์์ ์ด๋ฆ์ ์ ๊ณตํ๋ ํธ๋ฆฌํ App์ด ํฌํจ๋์ด ์๋ค. Pod ์ค์น๋ฅผ ์คํํ ํ ํ๋ก์ ํธ workspace์ ์ด๊ณ ์คํํด๋ณด์. ์ฑ์ด rayWenderrich Green์ผ๋ก ์์ํ๋ ๊ฒ์ ๋ณผ ์ ์์ง๋ง, ์์์ hex ์์ ์ฝ๋๋ฅผ ์ ๋ ฅํ๊ณ RGB์ ์ด๋ฆ์ ์ป์ ์ ์๋ค.
ํด๋น App์ 24 ์ฑํฐ "MVVM with RxSwift"์์ ๋ฐฐ์ธ MVVM ์ค๊ณ ํจํด์ ์ฌ์ฉํ์ฌ ๊ตฌ์ฑ๋๋ค. ViewModel์๋ ViewController๊ฐ View๋ฅผ ์ ์ดํ๋ ๋ฐ ์ฌ์ฉํ ๋ค์ ๋ ผ๋ฆฌ๊ฐ ํฌํจ๋์ด ์์ผ๋ฉฐ, ์ด ๋ ผ๋ฆฌ์ ๋ํ ํ ์คํธ๋ ์ฑํฐ์ ๋ท๋ถ๋ถ์ ์์ฑ๋๋ค.
// Convert hex text to color
color = hexString
.map { hex in
guard hex.count == 7 else { return .clear }
let color = UIColor(hex: hex)
return color
}
.asDriver(onErrorJustReturn: .clear)
// Convert the color to an rgb tuple
rgb = color
.map { color in
var red: CGFloat = 0.0
var green: CGFloat = 0.0
var blue: CGFloat = 0.0
color.getRed(&red, green: &green, blue: &blue, alpha: nil)
let rgb = (Int(red * 255.0), Int(green * 255.0), Int(blue *
255.0))
return rgb }
.asDriver(onErrorJustReturn: (0, 0, 0))
// Convert the hex text to a matching name
colorName = hexString
.map { hexString in
let hex = String(hexString.dropFirst())
if let color = ColorName(rawValue: hex) {
return "\(color)"
} else {
return "--" }
}
.asDriver(onErrorJustReturn: "")์ด ์ฝ๋๋ฅผ ํ ์คํธํ๊ธฐ ์ ์ RxSwift Operator์ ๋ํ ๋ช ๊ฐ์ง ํ ์คํธ๋ฅผ ์์ฑํ์ฌ RxTest์ ๋ํด ๋ฐฐ์ฐ๊ฒ ๋ ๊ฑฐ๋ค.
์ด๋ฒ ์ฑํฐ์์๋ XCTest๋ฅผ ์ฌ์ฉํ์ฌ iOS์์ ๋จ์ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๋ฐ ์ต์ํ๋ค๊ณ ์๊ฐํ๊ณ ์งํํ๋ค. iOS์์ ์ ๋ ํ ์คํธ๋ฅผ ์ฒ์ ํ๋ ๊ฒฝ์ฐ ๋น๋์ค ๊ณผ์ ์์ iOS ์ ๋ ๋ฐ UI ํ ์คํธ ์์์ ์ ์๊ฐํ๊ธธ ๋ฐ๋๋ค.
Testing operators with RxTest
RxTest๋ RxSwift์ ๋ณ๋์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค. RxSwift repo ๋ด์์ ํธ์คํ ๋์ง๋ง ๋ณ๋์ Pod ์ค์น ๋ฐ import๊ฐ ํ์ํฉ๋๋ค. RxTest๋ RxSwift ์ฝ๋๋ฅผ ํ ์คํธํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ ์ ์ฉํ ์ถ๊ฐ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
- Test Scheduler - ์๊ฐ ์ ํ ์์ ํ ์คํธ๋ฅผ ์ธ๋ถ์ ์ผ๋ก ์ ์ดํ ์ ์๋ ๊ฐ์ ์๊ฐ ์ค์ผ์ค๋ฌ.
- Recorded.next(::), Recorded.completed(::) ๋ฐ Recorded.error(::) - ํ ์คํธ์์ ์ง์ ๋ ์๊ฐ์ ์ด๋ฌํ ์ด๋ฒคํธ๋ฅผ ๊ด์ฐฐ ๊ฐ๋ฅํ ๊ณต์ฅ ๋ฉ์๋.
RxTest๋ Hot ๋ฐ Cold Observable ๋ง๋ค์ด๋ด์ด ์ถ๊ฐํ ์ ์๋ค.
What are hot and cold observables?
RxSwift๋ Rx ์ฝ๋๋ฅผ ๊ฐ์ํํ๊ณ ๋จ์ํํ๊ธฐ ์ํด ๋ง์ ๋
ธ๋ ฅ์ ๊ธฐ์ธ์ธ๋ค. RxSwift ์ปค๋ฎค๋ํฐ์๋ Hot ๋ฐ Cold Observable์ ๊ตฌ์ฒด์ ์ธ type ๋์ Observable์ ํน์ฑ์ผ๋ก ์๊ฐํด์ผ ํ๋ค๋ ์๊ฒฌ์ด ์๋ค.
์ด๊ฒ์ ๊ตฌํ ์ธ๋ถ ์ฌํญ์ด์ง๋ง, ํ
์คํธ๋ฅผ ์ ์ธํ๊ณ RxSwift์์ Hot ๋ฐ Cold Observable์ ๋ํ ๋ง์ ์ด์ผ๊ธฐ๋ค์ ๋ณผ ์ ์๊ธฐ ๋๋ฌธ์ ์ฃผ์ํ ํ์๊ฐ ์๋ค.
- Hot Observable:
- ๊ตฌ๋ ์๊ฐ ์๋์ง ์ฌ๋ถ์ ๊ด๊ณ์์ด ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉ.
- ๊ตฌ๋ ์๊ฐ ์๋์ง ์ฌ๋ถ์ ๊ด๊ณ์์ด ์์๋ฅผ ์์ฑ.
- ์ฃผ๋ก Behavior Relay์ ๊ฐ์ ์ํ ์ ์ฅ ์ ํ๊ณผ ํจ๊ป ์ฌ์ฉ.
- Cold Observable:
- ๊ตฌ๋ ์์๋ง ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉ.
- ๊ตฌ๋ ์๊ฐ ์๋ ๊ฒฝ์ฐ์๋ง ์์๋ฅผ ์์ฑ.
- ๋คํธ์ํน๊ณผ ๊ฐ์ ๋น๋๊ธฐ ์์ ์ ์ฃผ๋ก ์ฌ์ฉ.
๊ณง ์์ฑํ ๋จ์ ํ ์คํธ์์๋ Hot Observable๋ฅผ ์ฌ์ฉํ๊ฒ ์ง๋ง, ์ด๋ฌํ ์ฐจ์ด๋ฅผ ์๊ณ ์๋ ๊ฒฝ์ฐ Cold Observable์ ์ฌ์ฉํด์ผ ํ ์๋ ์๋ค.
class TestingOperators : XCTestCase {
var scheduler: TestScheduler!
var subscription: Disposable!
override func setUp() {
super.setUp()
// ํ
์คํธ ์ฌ๋ก๊ฐ ์์๋๊ธฐ ์ ์ ํธ์ถ๋๋ setUp() ๋ฉ์๋์์ ์ด๊ธฐ ํด๋ญ ๊ฐ 0์ผ๋ก ์ ์ค์ผ์ค๋ฌ๋ฅผ ์ด๊ธฐํ
scheduler = TestScheduler(initialClock: 0)
}
override func tearDown() {
// ํ
์คํธ ์ฌ๋ก๊ฐ ์ข
๋ฃ๋๊ณ ํ
์คํธ ๊ตฌ๋
์ 1000์ด์ ๊ฐ์ ์๊ฐ ๋จ์๋ก ํ๊ธฐํ๋๋ก ์์ฝํ๊ณ ์ค์ผ์ค๋ฌ๋ฅผ nil์ผ๋ก ์ค์ ํ์ฌ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ํด์
scheduler.scheduleAt(1000) {
self.subscription.dispose()
}
scheduler = nil
super.tearDown()
}
// amb(_:)๋ย amb(_:)๋ก ์ฎ์ธ ์ฌ๋ฌ ์ํ์ค ์ค์์ ๊ฐ์ฅ ๋จผ์ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํจ ์ํ์ค์ ์ด๋ฒคํธ๋ง ์ ๋ฌ
func test_Amb() {
let observer = scheduler.createObserver(String.self)
let observableA = scheduler.createHotObservable([
.next(100, "a"),
.next(200, "b"),
.next(300, "c")
])
let observableB = scheduler.createHotObservable([
.next(90, "1"),
.next(200, "2"),
.next(300, "3")
])
let ambObservable = observableA.amb(observableB)
self.subscription = ambObservable.subscribe(observer)
scheduler.start()
let results = observer.events.compactMap {
$0.value.element
}
XCTAssertEqual(results, ["1", "2", "3"])
}
// filter(_:)๋ ์ํ์ค ์ค์์ ์กฐ๊ฑด์ ์ผ์นํ๋ ์ด๋ฒคํธ๋ง ์ ๋ฌ
func test_Filter() {
let observer = scheduler.createObserver(Int.self)
let observable = scheduler.createHotObservable([
.next(100, 1),
.next(200, 2),
.next(300, 3),
.next(400, 2),
.next(500, 1)
])
let filterObservable = observable.filter {
$0 < 3
}
scheduler.scheduleAt(0) {
self.subscription = filterObservable.subscribe(observer)
}
scheduler.start()
let results = observer.events.compactMap {
$0.value.element
}
XCTAssertEqual(results, [1, 2, 2, 1])
}
}Using RxBlocking
RxBlocking์ ์์ฒด pod๊ฐ ์๋ RxSwift repo์ ํฌํจ๋ ๋ ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ฉฐ ๋ณ๋๋ก import ํด์ผํ๋ค.
์ฃผ์ ๋ชฉ์ ์ observable์ BlockingObservable๋ก toBlocking(timeout:) ๋ฉ์๋๋ฅผ ํตํด์ ๋ณํํ๋ ๊ฒ! ์ด๋ ๊ฒ ํ๋ฉด ์ ์์ ์ผ๋ก ๋๋ ์๊ฐ ์ด๊ณผ์ ๋๋ฌํ์ฌ ๊ด์ฐฐ ๊ฐ๋ฅํ ์ค๋ ๋๊ฐ ์ข ๋ฃ๋ ๋๊น์ง ํ์ฌ ์ค๋ ๋๋ฅผ ์ฐจ๋จํ๋ค.
timeout์ ์ ๋ฌํ๋ ์ธ์๋ optional TimeInterval ํ์ ์ผ๋ก ๊ธฐ๋ณธ๊ฐ์ nil์ด๋ค. ๋ง์ฝ timeout ๊ฐ์ ์ค์ ํ๊ณ observable์ด ์ ์์ ์ผ๋ก ์ข ๋ฃ๋๊ธฐ ์ ์ ํด๋น interval์ด ๊ฒฝ๊ณผํ๋ฉด, toBlocking ๋ฉ์๋์์๋ RxError.timeout ์ค๋ฅ๋ฅผ ๋์ง๊ฒ ๋๋ค. ์ด๋ ๋ณธ์ง์ ์ผ๋ก ๋น๋๊ธฐ์ ์์ ์ ๋๊ธฐ์ ์์ ์ผ๋ก ๋ณํํ๋ฏ๋ก ํ ์คํธ๊ฐ ํจ์ฌ ์ฌ์์ง๊ฒ ๋๋ค.
์๋ ์ฒซ ๋ฒ์งธ ์์ ์์ toBlocking() ์ฐ์ฐ์๋ ArrayObservable๋ก ๋ณํํ์ฌ ์ค์ผ์ค๋ฌ๊ฐ ์์ฑํ ์ค๋ ๋๋ฅผ ์ข ๋ฃํ ๋๊น์ง ์ฐจ๋จํ๋ค. ํ ์คํธ๋ฅผ ์คํํ๋ฉด ๋น๋๊ธฐ์ ์์ ์ ํ ์คํธํ๊ธฐ ์ํ ์ธ ์ค์ ์ฝ๋ ์ฑ๊ณต ์ฌ๋ถ๋ฅผ ํ์ธํ ์ ์๋ค.
public enum MaterializedSequenceResult<T> {
case completed(elements: [T])
case failed(elements: [T], error: Error)
}RxBlocking์๋ ์ฐจ๋จ ์์ ์ ๊ฒฐ๊ณผ๋ฅผ ์กฐ์ฌํ๋ ๋ฐ ์ฌ์ฉํ ์ ์๋ .materialize() operator๋ ์๋ค. completed / failed ๋ ๊ฐ์ ์ผ์ด์ค์ ๊ด๋ จ ๊ฐ์ ํฌํจํ๋ ์ด๊ฑฐํ ํ์ ์ธ MaterializedSequenceResult(MaterializedSequenceResult)๊ฐ ๋ฐํ๋๋ค.
observable์ด ์ฑ๊ณต์ ์ผ๋ก ์ข ๋ฃ๋๋ฉด, completed ์ผ์ด์ค์ ๊ฒฝ์ฐ ๊ธฐ๋ณธ observable๋ค๋ก๋ถํฐ ๋ฐฉ์ถ๋ ์์๋ค์ ๋ฐฐ์ด๊ณผ ๊ด๋ จ๋ ๊ฒ์ด๋ค. ๊ทธ๋ฆฌ๊ณ ์คํจํ๋ฉด, failed ์ผ์ด์ค๋ ์์ ๋ฐฐ์ด๊ณผ ์ค๋ฅ๋ฅผ ๋ชจ๋ ๊ด๋ จ ๋์ด ๋์ค๊ฒ ๋๋ค. ์๋ ๋ ๋ฒ์งธ ์์ ๋ฅผ ๋ณด์. ์์ ๋ materialize๋ฅผ ์ฌ์ฉํ์ฌ toArray ๋ํ ์ด์ ํ ์คํธ๋ฅผ ๋ค์ ๊ตฌํํ๋ค.
func test_ToArray() throws {
// ๊ธฐ๋ณธ qos๋ก ๋น๋๊ธฐ ํ
์คํธ๋ฅผ ์คํํ ๋์ ์ค์ผ์ค๋ฌ๋ฅผ ์์ฑ.
let scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
// ์ค์ผ์ค๋ฌ์์ ๋ ์ ์์ observable ๊ตฌ๋
ํ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ ํ observable์ ๋ง๋ ๋ค.
let toArrayObservable = Observable.of(1, 2).subscribeOn(scheduler)
// ArrayObservable์ ๋ํด Blocking()์ ํธ์ถํ ๊ฒฐ๊ณผ์ ๋ํด toArray๋ฅผ ์ฌ์ฉํ๊ณ , Array๋ก ๋ฐํ๋๋ ๊ฐ์ ์์๋ ๊ฒฐ๊ณผ์ expect ํ๋ค.
XCTAssertEqual(try toArrayObservable.toBlocking().toArray(), [1, 2])
}
func test_ToArrayMaterialized() {
// ์์ ๋์ผ
let scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
let toArrayObservable = Observable.of(1, 2).subscribeOn(scheduler)
// toBlocking()๊ณผ materialize()๋ฅผ observable์ ํธ์ถํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ํ ๋น
let result = toArrayObservable
.toBlocking()
.materialize()
// ์์๋ ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์ผ์ด์ค ๋ถ๋ฆฌ๋ฅผ ํตํ expect ๋ถ๊ธฐ ๋น๊ต
switch result {
case .completed(let elements):
XCTAssertEqual(elements, [1, 2])
case .failed(_, let error):
XCTFail(error.localizedDescription)
}
}ํ
์คํธ๋ฅผ ๋ค์ ์คํํ๊ณ ๋ชจ๋ ํ
์คํธ๊ฐ ์ฑ๊ณตํ๋์ง ํ์ธํด๋ณด์. ๋ณด๋ค์ํผ RxBlocking์ materialize() ์ฌ์ฉ๋ฒ์ RxSwift์ ๋ค๋ฅด์ง๋ง ๊ฐ๋
์ ์ผ๋ก๋ ์ ์ฌํ๋ค. RxBlocking ๋ฒ์ ์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ค ๊ฐ๋ ฅํ๊ณ ๋ช
์์ ์ผ๋ก ์กฐ์ฌํ๊ธฐ ์ํด ๊ฒฐ๊ณผ๋ฅผ ์ด๊ฑฐํ์ผ๋ก ๋ชจ๋ธ๋งํ๋ ์ถ๊ฐ ๋จ๊ณ๋ฅผ ๊ฑฐ์น๋ค.
๊ณง RxBlocking์ผ๋ก ๋ ๋ง์ ์์
์ ํ๊ฒ ๋๊ฒ ์ง๋ง, ์ด์ ๋ operator ํ
์คํธ์์ ๋ฒ์ด๋ ์ฑ์ ํ๋ก๋์
์ฝ๋์ ๋ํ ๋ช ๊ฐ์ง ํ
์คํธ๋ฅผ ์์ฑํด๋ณด์.
Testing RxSwift production code
import XCTest
import RxSwift
import RxCocoa
import RxTest
@testable import Testing
class TestingViewModel : XCTestCase {
var viewModel: ViewModel!
var scheduler: ConcurrentDispatchQueueScheduler!
override func setUp() {
super.setUp()
viewModel = ViewModel()
scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
}
func test_ColorIsRedWhenHexStringIsFF0000_async() {
let disposeBag = DisposeBag()
// ๋์ค์ ๋ฌ์ฑํ expectation๏ฟฝ ์์ฑ
let expect = expectation(description: #function)
// ์์๋๋ ํ
์คํธ ๊ฒฐ๊ณผ๋ฅผ ์์ฑ. ์์ ์์์ ๋นจ๊ฐ
let expectedColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)
// ๏ฟฝ๋์ค์ ํ ๋นํ ๊ฒฐ๊ณผ๋ฅผ ์ ์ธ
var result: UIColor!
// ๋ทฐ ๋ชจ๋ธ์ ์ปฌ๋ฌ์ ๋ํ ๊ตฌ๋
์ ๋ง๋ฌ. ๊ตฌ๋
์ ๋๋ผ์ด๋ฒ๊ฐ ์ด๊ธฐ ์์๋ฅผ ์ฌ์ํ๊ธฐ ๋๋ฌธ์ ์ฒซ ๋ฒ์งธ ์์๋ ๊ฑด๋๋.
viewModel.color.asObservable()
.skip(1)
.subscribe(onNext: {
// ๋ค์ ์ด๋ฒคํธ ์์๋ฅผ ๊ฒฐ๊ณผ์ ํ ๋นํ๊ณ expect์ fulfill()์ ํธ์ถํฉ๋๋ค.
result = $0
expect.fulfill()
})
.disposed(by: disposeBag)
// ๋ทฐ ๋ชจ๋ธ์ hexString์ "#ff0000" ๋ฌธ์์ด ์ฃผ์
viewModel.hexString.accept("#ff0000")
// 1์ด๊ฐ์ ํ์์์์ผ๋ก ๊ธฐ๋์ ๋ถ์ํ ๋๊น์ง ๋๊ธฐ. ํด๋ก์ ์์ error๋ฅผ guard
waitForExpectations(timeout: 1.0) { error in
guard error == nil else {
XCTFail(error!.localizedDescription)
return
}
// ๋ค์ ์์ ์์์ด ์ค์ ๊ฒฐ๊ณผ์ ๊ฐ๋ค๊ณ expect ๋น๊ต
XCTAssertEqual(expectedColor, result)
}
}
func test_ColorIsRedWhenHexStringIsFF0000() throws {
// colorObservable์ ๋ง๋ค์ด ๋์ ์ค์ผ์ค๋ฌ์์ ๊ตฌ๋
ํ ๊ฒฐ๊ณผ๋ฅผ ์ ์ง
let colorObservable = viewModel.color.asObservable().subscribeOn(scheduler)
// ๋ทฐ ๋ชจ๋ธ์ hexString์ "#ff0000" ๋ฌธ์์ด ์ฃผ์
viewModel.hexString.accept("#ff0000")
// colorObservable๋ฅผ toBlocking()ํ๊ณ ์ฒซ ๋ฒ์งธ ์์๊ฐ ๋ฐฉ์ถ๋ ๋๊น์ง ๋๊ธฐ. ์์ ์์์ ๊ฒฐ๊ณผ์ ๊ฐ๋ค๊ณ expect ๋น๊ต
XCTAssertEqual(try colorObservable.toBlocking(timeout: 1.0).first(),
.red)
}
func test_RgbIs010WhenHexStringIs00FF00() throws {
// ์ค์ผ์ค๋ฌ์์ ๊ตฌ๋
ํ rgbObservable์ ์์ฑ
let rgbObservable = viewModel.rgb.asObservable().subscribeOn(scheduler)
// ๋ทฐ ๋ชจ๋ธ์ hexString์ "#00ff00" ๋ฌธ์์ด ์ฃผ์
viewModel.hexString.accept("#00ff00")
// blocking ๋ rgbOservable๋ก ํธ์ถํ ์ฒซ ๋ฒ์งธ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ์ํ ๋ค์ ๊ฐ ๊ฐ์ด ๊ฒฐ๊ณผ์ ๊ฐ๋ค๊ณ expect ๋น๊ต
let result = try rgbObservable.toBlocking().first()!
// 0์์ 1๋ก, 0์์ 255๋ก ๋ณํํ ๊ฒ์ ๋จ์ง ํ
์คํธ ์ด๋ฆ๊ณผ ์ผ์นํ๊ณ ๋ฐ๋ผ ํ๊ธฐ ์ฝ๊ฒ ํ๊ธฐ ์ํด
XCTAssertEqual(0 * 255, result.0)
XCTAssertEqual(1 * 255, result.1)
XCTAssertEqual(0 * 255, result.2)
}
func testColorNameIsRayWenderlichGreenWhenHexStringIs006636() throws {
// ์ค์ผ์ค๋ฌ์์ ๊ตฌ๋
ํ colorNameObservable์ ์์ฑ
let colorNameObservable = viewModel.colorName.asObservable().subscribeOn(scheduler)
// ๋ทฐ ๋ชจ๋ธ์ hexString์ "#006636" ๋ฌธ์์ด ์ฃผ์
viewModel.hexString.accept("#006636")
// ๋ค์ ๊ฐ ๊ฐ์ด ๊ฒฐ๊ณผ์ ๊ฐ๋ค๊ณ expect ๋น๊ต
XCTAssertEqual("rayWenderlichGreen", try colorNameObservable.toBlocking().first()!)
}
}"ํน๊ตฌ๊ณ ๋ฐ๋ณตํ๋ผ"๋ ๋ง์ด ๋ ์ค๋ฅด์ง๋ง ์ข์ ์๋ฏธ๋ก ์ฝํ๋ค. ํ
์คํธ๋ ํญ์ ์ด๋ ๊ฒ ์ฌ์์ผ ํ๋ค.
Command-U๋ฅผ ๋๋ฌ ์ด ํ๋ก์ ํธ์ ๋ชจ๋ ํ
์คํธ๋ฅผ ์คํํ๋ฉด ๋ชจ๋ ๊ฒ์ด ์ ์ ๋ฌ๋๋ค
Where to go from here?
RxText ๋ฐ RxBlocking์ ์ฌ์ฉํ์ฌ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๊ฒ์ RxSwift ๋ฐ RxCocoa๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ ๋ฐ UI ๋ฐ์ธ๋ฉ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ๊ณผ ์ ์ฌํ๋ค. 24 ์ฑํฐ "MVVM with RxSwift"์์ ๋ ๋ง์ ๋ทฐ ๋ชจ๋ธ ํ ์คํธ๋ฅผ ์ํํ ์์ ์ด๋ฉฐ ๊ทธ ๋ ๊ฐ์ ๋ค์ ๋ง๋๋ณด์
