Test Scheduler

โฐ TestScheduler ์‹ฌํ™” ๊ฐ€์ด๋“œ โ€” ๊ฐ€์ƒ ์‹œ๊ฐ„ ์™„์ „ ์ •๋ณต

โ€œ์ฝ”๋“œ๋ฅผ ๋А๋ฆฌ๊ฒŒ ๋งŒ๋“ค์ง€ ๋ง๊ณ , ์‹œ๊ฐ„์„ ๋น ๋ฅด๊ฒŒ ๋Œ๋ ค๋ผ.โ€

TestScheduler๋Š” RxTest๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ฐ€์ƒ ์‹œ๊ณ„๋กœ, ๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„ ๋น„๋™๊ธฐ ๋กœ์ง๋„ ์ฆ‰์‹œ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด ์ค๋‹ˆ๋‹ค. ์ด ๋ฌธ์„œ๋Š” APIยทํŒจํ„ดยท๋””๋ฒ„๊น… ๋…ธํ•˜์šฐ๋ฅผ ํ•œ๊ธ€๋กœ ์ž์„ธํžˆ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.


1๏ธโƒฃ ๊ธฐ๋ณธ ๊ฐœ๋… ๋‹ค์‹œ ๋ณด๊ธฐ

๊ฐœ๋…
์„ค๋ช…

Tick(ํ‹ฑ)

TestScheduler๊ฐ€ ์ •์˜ํ•œ ๊ฐ€์ƒ ์‹œ๊ฐ„ ๋‹จ์œ„. ๋ณดํ†ต 1ํ‹ฑ=10ms ๋กœ ๊ฐ€์ •ํ•˜์ง€๋งŒ, ์‹ค์ œ ๋‹จ์œ„๋Š” ๊ฐœ๋ฐœ์ž ์ž์œ .

Clock

ํ˜„์žฌ ๊ฐ€์ƒ ์‹œ๊ฐ(์ •์ˆ˜). advanceTo(50) โ†’ 50ํ‹ฑ์œผ๋กœ ๋ฐ”๋กœ ์ด๋™.

Recorded

(time: Int, event: Event<Element>) ๊ตฌ์กฐ. ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผยท์ž…๋ ฅ ๋ชจ๋‘ Recorded ๋ฐฐ์—ด๋กœ ํ‘œํ˜„.


2๏ธโƒฃ ํ•„์ˆ˜ ๋ฉ”์„œ๋“œ ํ•œ๋ˆˆ์— ์ •๋ฆฌ

๋ฉ”์„œ๋“œ
์šฉ๋„
์˜ˆ์‹œ

createHotObservable(_:)

ํ…Œ์ŠคํŠธ ์ž…๋ ฅ ์ •์˜(๊ตฌ๋… ์ „๋ถ€ํ„ฐ ์ด๋ฒคํŠธ ์˜ˆ์•ฝ)

.next(10, "a")

createColdObservable(_:)

๊ตฌ๋… ์‹œ 0ํ‹ฑ๋ถ€ํ„ฐ ์นด์šดํŠธ

.next(5, 1)

start(created:subscribed:disposed:)

ํŽธ์˜ ๋ฉ”์„œ๋“œ. Observable ๋งŒ๋“ค๊ณ  200ํ‹ฑ๊นŒ์ง€ ์‹คํ–‰ํ•ด ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜

scheduler.start { myStream }

advanceTo(_:)

ํด๋Ÿญ์„ ํŠน์ • ์‹œ๊ฐ„์œผ๋กœ ์ ํ”„

scheduler.advanceTo(150)

createObserver(Element.self)

๋นˆ Recorder. ์ŠคํŠธ๋ฆผ์„ ์ˆ˜๋™์œผ๋กœ ๊ตฌ๋… ํ›„ ๊ฒฐ๊ณผ ์ˆ˜์ง‘

let res = scheduler.createObserver(String.self)

scheduleAt(_:action:)

ํŠน์ • ์‹œ์ ์— ์ฝ”๋“œ ์‹คํ–‰

scheduler.scheduleAt(50) { subject.onNext(1) }


3๏ธโƒฃ ๋‹จ๊ณ„๋ณ„ ์ปค์Šคํ…€ ํ…Œ์ŠคํŠธ ํ…œํ”Œ๋ฆฟ

func testCustomOperator() {
    // 1) ๊ฐ€์ƒ ์‹œ๊ณ„ & Observer
    let scheduler = TestScheduler(initialClock: 0)
    let observer = scheduler.createObserver(Int.self)

    // 2) ์ž…๋ ฅ ์ •์˜ (Cold)
    let numbers = scheduler.createColdObservable([
        .next(5, 1), .next(15, 2), .next(25, 3), .completed(35)
    ])

    // 3) ์‹œ์Šคํ…œ ์–ธ๋”ํ…Œ์ŠคํŠธ(SUT)
    let sut = numbers.scan(0, +) // ๋ˆ„์  ํ•ฉ

    // 4) ๊ตฌ๋… ์Šค์ผ€์ค„๋ง
    scheduler.scheduleAt(0) {
        sut.bind(to: observer).disposed(by: DisposeBag())
    }

    // 5) ๊ฐ€์ƒ ์‹œ๊ณ„ ์‹คํ–‰ (0~100ํ‹ฑ)
    scheduler.start()

    // 6) ๊ธฐ๋Œ€๊ฐ’
    let expected = [
        .next(5, 1), .next(15, 3), .next(25, 6), .completed(35)
    ]

    XCTAssertEqual(observer.events, expected)
}

4๏ธโƒฃ advanceTo vs start ์ฐจ์ด

ํŠน์ง•

start

advanceTo + ์ˆ˜๋™ ๊ตฌ๋…

๊ตฌ๋… ์‹œ์ 

์ž๋™ (subscribed: ํŒŒ๋ผ๋ฏธํ„ฐ, ๊ธฐ๋ณธ 200)

์ง์ ‘ scheduleAt ์œผ๋กœ ์ง€์ •

๋””์Šคํฌ์ฆˆ

์ž๋™ (disposed: ํŒŒ๋ผ๋ฏธํ„ฐ, ๊ธฐ๋ณธ 1000)

์ˆ˜๋™ dispose ๊ฐ€๋Šฅ

์‚ฌ์šฉ ๋‚œ์ด๋„

๊ฐ„ํŽธ(์›๋ผ์ธ)

์œ ์—ฐ(๋ณต์žก ์‹œ๋‚˜๋ฆฌ์˜ค)

๋Œ€๊ทœ๋ชจ ์‹œ๋‚˜๋ฆฌ์˜คยท์—ฌ๋Ÿฌ ๋‹จ๊ณ„ ๊ตฌ๋…/ํ•ด์ œ๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋• advanceTo ๋ฐฉ์‹์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.


5๏ธโƒฃ ์‹ค์ „: ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ์žฌ์‹œ๋„ ํ…Œ์ŠคํŠธ

func testExponentialBackoff() {
    enum MyErr: Error { case fail }

    let scheduler = TestScheduler(initialClock: 0)
    let source = scheduler.createColdObservable([
        .error(10, MyErr.fail)
    ])

    // 1,2,4ํ‹ฑ ์ง€์—ฐ ํ›„ ์žฌ์‹œ๋„ (์ด 4ํšŒ)
    let backoff = source.retryWhen { errors in
        errors.enumerated().flatMap { attempt, error -> Observable<Int> in
            guard attempt < 3 else { return Observable.error(error) }
            let delay = Int(pow(2.0, Double(attempt)))
            return Observable<Int>.timer(.seconds(delay), scheduler: scheduler)
        }
    }

    let res = scheduler.start(created: 0, subscribed: 0, disposed: 50) { backoff }

    // 0+10=10  error
    // retry1 delay1  โ†’ 11 error(21)
    // retry2 delay2  โ†’ 23 error(33)
    // retry3 delay4  โ†’ 37 error(47)
    XCTAssertEqual(res.events.last?.time, 47)
}

6๏ธโƒฃ ๋””๋ฒ„๊ทธ ํŒ ๐Ÿ› ๏ธ

  1. ํด๋Ÿญ ๋กœ๊น… : print(scheduler.clock) ์ค‘๊ฐ„ ํ™•์ธ

  2. Recorded ๋ฐฐ์—ด ์ถœ๋ ฅ : observer.events.forEach(print) ๋กœ ์ˆœ์„œ ์ฒดํฌ

  3. ๋ถˆํ•„์š”ํ•œ ๋งˆ๋ธ” ์ค‘๋ณต ์ œ๊ฑฐ : ๋ฐ˜๋ณต๋˜๋Š” ํŒจํ„ด์€ generateRecorded ํ—ฌํผ ํ•จ์ˆ˜ ์‚ฌ์šฉ


7๏ธโƒฃ ์ฒดํฌ๋ฆฌ์ŠคํŠธ โœ…

์ฒดํฌ
์„ค๋ช…

Cold/Hot ์œ ํ˜• ์ดํ•ด

๊ตฌ๋… ์‹œ์ ์ด ๊ฒฐ๊ณผ์— ๋ฏธ์น˜๋Š” ์˜ํ–ฅ ํŒŒ์•…

์‹œ๊ฐ„ ๋‹จ์œ„ ์ผ๊ด€์„ฑ

1ํ‹ฑ=10ms ๋“ฑ ํ”„๋กœ์ ํŠธ ๊ณตํ†ต ๊ทœ์น™

start ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆ˜์ •

created, subscribed, disposed ํ•„์š”์— ๋งž๊ฒŒ ์กฐ์ •

Equatable ๊ตฌํ˜„

์ปค์Šคํ…€ ๋ชจ๋ธ ๋น„๊ต ์ •ํ™•๋„ ํ™•๋ณด

๋‹จ์ผ ์ฑ…์ž„ ํ…Œ์ŠคํŠธ

ํ•˜๋‚˜์˜ ์—ฐ์‚ฐ์ž/๊ฒฝ๋กœ๋งŒ ๊ฒ€์ฆํ•ด ์‹คํŒจ ์›์ธ ๋ช…ํ™•ํ™”


8๏ธโƒฃ Mini Quiz

  1. createObserver ๋กœ ์ˆ˜์ง‘ํ•œ Recorded ์ด๋ฒคํŠธ๋ฅผ scheduler.advanceTo(100) ์ „์— ์กฐํšŒํ•˜๋ฉด ์–ด๋–ค ๊ฐ’์ด ์žˆ๋‚˜์š”?

  2. start(subscribed: 50) ๋กœ ์„ค์ •ํ•˜๋ฉด .next(10, x) Cold ์ด๋ฒคํŠธ๋Š” ๋ช‡ ํ‹ฑ์— ๋ฐœ์ƒํ•˜๋‚˜์š”?

  3. Hot Observable ํ…Œ์ŠคํŠธ์—์„œ scheduleAt(0) ๊ณผ scheduleAt(100) ๊ตฌ๋… ์‹œ ๊ฒฐ๊ณผ ์ฐจ์ด๋Š”?

Answers
  1. advanceTo ์ „์—” ์ด๋ฒคํŠธ๊ฐ€ ์•„์ง ๋ฐœ์ƒํ•˜์ง€ ์•Š์•„ ๋ฐฐ์—ด์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

  2. 50(๊ตฌ๋…) + 10 = 60ํ‹ฑ ์— onNext.

  3. 100ํ‹ฑ ๊ตฌ๋…์ด๋ฉด ๊ตฌ๋… ์ด์ „์— ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ๋Š” ์ˆ˜์‹ ๋˜์ง€ ์•Š์•„ ์ผ๋ถ€ ๊ฐ’์ด ๋ˆ„๋ฝ๋ฉ๋‹ˆ๋‹ค.


์ด์ œ TestScheduler๊นŒ์ง€ ๋งˆ์Šคํ„ฐํ–ˆ์Šต๋‹ˆ๋‹ค! ์‹ค์ œ ํ”„๋กœ์ ํŠธ์˜ ๋น„๋™๊ธฐ ์ฝ”๋“œ๋ฅผ ์ž์‹  ์žˆ๊ฒŒ ํ…Œ์ŠคํŠธํ•ด ๋ณด์„ธ์š”. ๐Ÿš€

Last updated