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 ์˜ ์˜๋ฏธ

  • ๊ตฌ๋…์ž๊ฐ€ 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

  1. ๋งˆ๋ธ” --(ab)-| ๋Š” ์–ด๋–ค ์ˆœ์„œ๋กœ ์ด๋ฒคํŠธ๊ฐ€ ์ „๋‹ฌ๋˜๋‚˜์š”?

  2. TestScheduler.start ์˜ ๊ธฐ๋ณธ ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ข…๋ฃŒ ์‹œ๊ฐ์€ ๋ช‡ ํ‹ฑ์ผ๊นŒ์š”?

  3. Hot ์ŠคํŠธ๋ฆผ ํ…Œ์ŠคํŠธ ์‹œ, ๊ตฌ๋…์„ scheduler.scheduleAt(5) ๋Œ€์‹  scheduler.scheduleAt(25) ์— ํ•˜๋ฉด ์–ด๋–ค ์ฐจ์ด๊ฐ€ ์žˆ๋‚˜์š”?

Answers
  1. 20ํ‹ฑ ํ›„ ๋™์‹œ์— a, b ์ด๋ฒคํŠธ(onNext ์ˆœ์„œ๋Š” ๋ฐฐ์—ด ์ •์˜ ์ˆœ), ๊ทธ ๋‹ค์Œ 10ํ‹ฑ ํ›„ onCompleted.

  2. ๊ธฐ๋ณธ๊ฐ’ 200ํ‹ฑ (ํ•„์š” ์‹œ disposed: ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ˆ˜์ • ๊ฐ€๋Šฅ).

  3. 25ํ‹ฑ์— ๊ตฌ๋…ํ•˜๋ฉด 5ยท15ํ‹ฑ ์ด๋ฒคํŠธ(X,Y)๊ฐ€ ์ด๋ฏธ ๋ฐœํ–‰๋ผ ์ˆ˜์‹ ๋˜์ง€ ์•Š์Œ.


๋‹ค์Œ ๋ฌธ์„œ โ–ถ๏ธ test_scheduler.md ์—์„œ TestScheduler API ๋ฅผ ๋”์šฑ ๊นŠ์ด ๋‹ค๋ค„๋ด…์‹œ๋‹ค. ๐Ÿš€

Last updated