Marble Testing
๐ช Marble ํ
์คํธ ์์ ์
๋ฌธ
โ์คํธ๋ฆผ์ ๊ทธ๋ฆผ์ผ๋ก ๊ทธ๋ ค์ ๋๋ฒ๊น ํ๊ณ , ๋จ์ ํ ์คํธ๊น์ง!โ
์ด ๋ฌธ์๋ Marble Diagram์ ์ฒ์ ์ ํ๋ ๋ถ๋ ๋ฐ๋ก ์ดํดํ ์ ์๋๋ก, ๊ธฐํธ ์ค๋ช โ ์ฝ๋ ์์ฑ โ ํ ์คํธ ๊ฒฐ๊ณผ ๋น๊ต ์์ผ๋ก ์น์ ํ๊ฒ ์๋ดํฉ๋๋ค.
1๏ธโฃ Marble ๋ค์ด์ด๊ทธ๋จ์ด๋?
Marble(๊ตฌ์ฌ) : ์คํธ๋ฆผ์์ ๋ฐฉ์ถ๋๋ ์ด๋ฒคํธ ํ ๊ฐ๋ฅผ ๊ตฌ์ฌ ๋ชจ์์ผ๋ก ํํ
ํ์๋ผ์ธ(โ) : ์ผ์ชฝ์์ ์ค๋ฅธ์ชฝ์ผ๋ก ํ๋ฅด๋ ๊ฐ์์ ์๊ฐ ์ถ
--a-b--|
-
= 10โฏms ๊ฐ์ ์๊ฐ ๊ฐ๊ฒฉ ํ ์นธ (๋จ์๋ ํ ์คํธ์์ ์์ ๋กญ๊ฒ ์ ์)a
,b
= onNext ์ด๋ฒคํธ ๊ฐ (์ํ๋ฒณ / ์ซ์ / ์ด๋ชจ์ง ๋ฌด์์ด๋ ๊ฐ๋ฅ)|
= onCompleted (์คํธ๋ฆผ ์ ์ ์ข ๋ฃ)#
๋๋x
= onError (์๋ฌ๋ก ์ข ๋ฃ)(ab)
= ๊ฐ์ ์๊ฐ์นธ์ ๋์์ ๋ ์ด๋ฒคํธ ๋ฐ์
๐ก ์ค์ ์ฑ์์๋ ๋ฐ๋ฆฌ์ด๋ณด๋ค ํจ์ฌ ์งง์ ๊ฐ์ ์๊ฐ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์, ํ ์คํธ๊ฐ ๋งค์ฐ ๋น ๋ฅด๊ฒ ์คํ๋ฉ๋๋ค.
2๏ธโฃ RxTest ๊ธฐ๋ณธ ์ค์
import XCTest
import RxSwift
import RxTest
import RxBlocking // ํ์ ์ ๋๊ธฐ ๊ฒ์ฆ์ฉ
final class MarbleExampleTests: XCTestCase {
var scheduler: TestScheduler!
var disposeBag: DisposeBag!
override func setUp() {
super.setUp()
// 0 ์ ์์ ์๊ฐ์ผ๋ก ํ๋ ๊ฐ์ ์๊ณ ์์ฑ
scheduler = TestScheduler(initialClock: 0)
disposeBag = DisposeBag()
}
}
TestScheduler : ๊ฐ์ ์๊ฐ ์ปจํธ๋กค๋ฌ (tick ๋จ์: 10 by default)
DisposeBag : ํ ์คํธ๋ง๋ค ์๋ก ์์ฑํด ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง
3๏ธโฃ Recorded ์ด๋ฒคํธ ํ์
์ดํดํ๊ธฐ
RxTest๋ ํ์์คํฌํ + Event๋ฅผ Recorded
๊ตฌ์กฐ์ฒด๋ก ํํํฉ๋๋ค.
Recorded.next(20, "a") // 20ํฑ ์์ , onNext("a")
Recorded.completed(50) // 50ํฑ ์์ , onCompleted
Recorded.error(30, MyErr) // 30ํฑ ์์ , onError
.next
์ ์๋ฏธ
.next
์ ์๋ฏธ๊ตฌ๋ ์๊ฐ
onNext(value)
์ฝ๋ฐฑ์ ๋ฐ์ ์ ํํ ์์ ๊ณผ ๊ฐ์ ๊ธฐ๋กํฉ๋๋ค.์ด๋ฒคํธ๊ฐ ๊ฐ์ฒด๋ผ๋ฉด, ๋์ผ์ฑ ๋น๊ต๋ฅผ ์ํด Equatable ์ฑํ ํ์.
4๏ธโฃ Cold vs Hot Observable ์์
let cold = scheduler.createColdObservable([
.next(10, "A"), .next(20, "B"), .completed(30)
])
let hot = scheduler.createHotObservable([
.next(5, "X"), .next(15, "Y"), .completed(40)
])
Cold : ๊ตฌ๋ ํ ์๊ฐ๋ถํฐ 0ํฑ์ผ๋ก ์นด์ดํธ (ํ ์คํธ์ ์ถ์ฒ)
Hot : ํ ์คํธ ์๊ณ 0๋ถํฐ ์ด๋ฒคํธ๊ฐ ์์ฝ (๊ตฌ๋ ์ด ๋ฆ์ผ๋ฉด ์ผ๋ถ ๊ฐ ์์ค)
5๏ธโฃ ์ค์ ํ
์คํธ ์์ฑ โ debounce ์ฐ์ฐ์ ๊ฒ์ฆ (์์ ๋ฒ์ )
๋ชฉํ: debounce(15)
๊ฐ ๋ง์ง๋ง ์
๋ ฅ๋ง ์ ๋ฌํ๋์ง ํ์ธํฉ๋๋ค.
โ ๏ธ ์ค๊ฐ์ ์ ์
๋ ฅ์ด ๋ค์ด์ค๋ฉด ์ด์ ํ์ด๋จธ๊ฐ ๋ฆฌ์
๋๋ค๋ ์ ์ด ํต์ฌ์
๋๋ค.
func testDebounce() {
// 1) ์
๋ ฅ ์คํธ๋ฆผ ์ ์ (Hot)
// ํ์๋ผ์ธ: 10 โaโ 20 โbโโโโโโโโ 40 โcโโโโโโโโ 60(|)
let input = scheduler.createHotObservable([
.next(10, "a"), // a
.next(20, "b"), // b (a ํ์ด๋จธ ๋ฆฌ์
)
.next(40, "c"), // c
.completed(60) // ์๋ฃ
])
// 2) Operator ์ ์ฉ
let output = scheduler.start { // ๊ธฐ๋ณธ 0~200ํฑ ์คํ
input.debounce(.seconds(15), scheduler: scheduler) // 15ํฑ = 150โฏms ๊ฐ์
}
// 3) ๊ธฐ๋ ๊ฒฐ๊ณผ
// b : 20 + 15 = 35
// c : 40 + 15 = 55 (c ์ดํ 15ํฑ ๋์ ์ถ๊ฐ ์
๋ ฅ ์์)
let expected = [
Recorded.next(35, "b"),
Recorded.next(55, "c"),
Recorded.completed(60)
]
XCTAssertEqual(output.events, expected)
}
6๏ธโฃ Marble ๋ฌธ์์ด ํฌํผ๋ก ๊ฐ๋จํ๊ฒ (์ ํ) ํฌํผ๋ก ๊ฐ๋จํ๊ฒ (์ ํ)
let cold = scheduler.createColdObservable(marbles: "--a-b--|", values: ["a":1, "b":2])
์๋ํํฐ
RxMarbles
๋๋RxExpect
๋ฑ์ ์ฌ์ฉํ๋ฉด ๋ฌธ์์ด๋ง์ผ๋ก ํ ์คํธ ์ ์ ๊ฐ๋ฅ
7๏ธโฃ ์ค๋ฅ ๋ฐ ์ฌ์๋ ํ
์คํธ ์์ โ ํ์๋ผ์ธ ์์ธ ์ค๋ช
retry(2)
๋ ์ต๋ 2ํ ์ถ๊ฐ ์ฌ์๋(์ด 3ํ ์คํ) ๋ฅผ ์๋ฏธํฉ๋๋ค. TestScheduler.start
๋ ๊ธฐ๋ณธ์ ์ผ๋ก 200ํฑ ์์ ์ Cold Observable ์ ๊ตฌ๋
ํฉ๋๋ค. ๋ฐ๋ผ์ ๋ด๋ถ ์ด๋ฒคํธ ์๊ฐ +200
์ผ๋ก ๊ณ์ฐ๋ฉ๋๋ค.
์๊ฐ | 200 210 220 230 240 250 260 270
| โ๏ธ 1 X โ๏ธ 1 X โ๏ธ 1 X
ํ์ฐจ | 1st 2nd 3rd
๊ฐ ํ์ฐจ: ๊ฐ(10ํฑ) โ ์๋ฌ(20ํฑ)
3ํ์ฐจ(๋ง์ง๋ง) ์๋ฌ ๋ฐ์ ํ
retry
์ข ๋ฃ
func testRetryLogic() {
enum TestErr: Error { case fail }
// Cold Observable: 10ํฑ ํ ๊ฐ, 20ํฑ ํ ์๋ฌ
let failing = scheduler.createColdObservable([
.next(10, 1),
.error(20, TestErr.fail)
])
let output = scheduler.start { failing.retry(2) } // ์ด 3ํ ์คํ
let expected = [
// 1ํ์ฐจ (200 + 10/20)
.next(210, 1), .error(220, TestErr.fail),
// 2ํ์ฐจ (220 dispose + ์ฆ์ ์ฌ๊ตฌ๋
โ 200+30=230,200+40=240)
.next(230, 1), .error(240, TestErr.fail),
// 3ํ์ฐจ (240 dispose ํ ์ฌ๊ตฌ๋
)
.next(250, 1), .error(260, TestErr.fail)
]
XCTAssertEqual(output.events, expected)
}
TIP: ๋ ๊ธด ์คํธ๋ฆผ์ด๋ ์ง์ ๋ฐฑ์คํ๋ฅผ ํ ์คํธํ ๋
retryWhen
๊ณผTestScheduler.advanceTo()
๋ฅผ ์กฐํฉํ์ธ์.
8๏ธโฃ ๋ฒ ์คํธ ํ๋ํฐ์ค ์ฒดํฌ๋ฆฌ์คํธ โ
Cold Observable ์ฐ์ ์ฌ์ฉ
Hot ์คํธ๋ฆผ์ ๊ตฌ๋ ์์ ์ ๋ฐ๋ผ ๊ฒฐ๊ณผ ๋ฌ๋ผ์ ธ ์ฌํ์ฑ ๋จ์ด์ง
ํ
์คํธ๋ง๋ค ** ยท **
์ฌ์์ฑ
ํ ์คํธ ๊ฐ ์ํ ๊ณต์ ๋ก ์ธํ ๊ฐ์ญ ๋ฐฉ์ง
ํฑ ๊ณ์ฐ์ ์ข ์ด์ ๋จผ์ ์์ฑ
์๊ฐ ๊ธฐ๋ฐ ์คํผ๋ ์ดํฐ(debounce
, throttle
) ์ค์ ๊ฐ์
``** ํ๋ผ๋ฏธํฐ ์กฐ์ **
200ํฑ ์ดํ ์ด๋ฒคํธ๋ ๊ฒ์ฆ ๊ฐ๋ฅ
์ปค์คํ ๋ชจ๋ธ Equatable ๊ตฌํ
XCTAssertEqual
๋น๊ต ์ค๋ฅ ๋ฐฉ์ง
Hot ์คํธ๋ฆผ ๊ตฌ๋ ์์ ๋ช ์ (``)
์ด๋ฒคํธ ์์ค ์ฌ๋ถ ๋ช ํํ
์ค์ ๋ก์ง๊ณผ ๋์ผ Scheduler ์ฌ์ฉ
observe(on:)
๋๋ฝ์ผ๋ก ์ธํ ์ค๋ ๋ ์ฐจ์ด ๋ฐฉ์ง
9๏ธโฃ Mini Quiz
๋ง๋ธ
--(ab)-|
๋ ์ด๋ค ์์๋ก ์ด๋ฒคํธ๊ฐ ์ ๋ฌ๋๋์?TestScheduler.start
์ ๊ธฐ๋ณธ ํ ์คํธ ์คํ ์ข ๋ฃ ์๊ฐ์ ๋ช ํฑ์ผ๊น์?Hot ์คํธ๋ฆผ ํ ์คํธ ์, ๊ตฌ๋ ์
scheduler.scheduleAt(5)
๋์scheduler.scheduleAt(25)
์ ํ๋ฉด ์ด๋ค ์ฐจ์ด๊ฐ ์๋์?
๋ค์ ๋ฌธ์ โถ๏ธ test_scheduler.md ์์ TestScheduler API ๋ฅผ ๋์ฑ ๊น์ด ๋ค๋ค๋ด ์๋ค. ๐
Last updated