Skip to content

Testing with RxTestย #33

@simoniful

Description

@simoniful

Ch.16 Testing with RxTest

๐Ÿ‘๐Ÿป๐Ÿ’ฏ๐Ÿš€
โ˜๐Ÿป ์ด ์žฅ์„ ๋†“์น˜์ง€ ๋งˆ๋ผ. ์—ฐ๊ตฌ ๊ฒฐ๊ณผ์— ๋”ฐ๋ฅด๋ฉด ๊ฐœ๋ฐœ์ž๊ฐ€ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์„ ๊ฑด๋„ˆ๋›ฐ๋Š” ์ด์œ ๋Š” ๋‘ ๊ฐ€์ง€๋‹ค.

  1. ๋ฒ„๊ทธ๊ฐ€ ์—†๋Š” ์ฝ”๋“œ๋ฅผ ์“ด๋‹ค.
  2. ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์€ ์žฌ๋ฏธ์—†๋‹ค.

์ฒซ ๋ฒˆ์งธ ์ด์œ ๊ฐ€ ๋‹น์‹ ์—๊ฒŒ ํ•ด๋‹น๋œ๋‹ค๋ฉด ๋ฐ”๋กœ ๊ณ ์šฉ๋  ๊ฒƒ!
๋‘ ๋ฒˆ์งธ ์ด์œ ์— ๋™์˜ํ•˜๋ฉด RxTest๋ฅผ ๋ณด์ž!

์ฑ…์„ ์ฝ๊ธฐ ์‹œ์ž‘ํ•˜๊ณ  App ํ”„๋กœ์ ํŠธ์—์„œ RxSwift๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹œ์ž‘ํ•œ ๊ฑฐ ์ž์ฒด๋กœ, RxTest์™€ RxBlocking๋ฅผ ํ†ตํ•ด RxSwift ์ฝ”๋“œ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. ์ž์ฒด๋กœ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์„ ์‰ฝ๊ณ  ์žฌ๋ฏธ์žˆ๊ฒŒ ๋งŒ๋“œ๋Š” ์šฐ์•„ํ•œ API๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

์ด๋ฒˆ ์ฑ•ํ„ฐ์—์„œ๋Š” RxTest ๋ฐ RxBlocking์— ๋Œ€ํ•ด ์•Œ์•„๋ณผ ๊ฑฐ๋‹ค. iOS ์•ฑ ํ”„๋กœ์ ํŠธ์—์„œ ์—ฌ๋Ÿฌ RxSwift ์—ฐ์‚ฐ์ž์™€ ์ƒ์‚ฐ RxSwift ์ฝ”๋“œ๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ์˜ˆ์ œ๋ฅผ ํฌํ•จํ•ด์„œ ๋ง์ด๋‹ค.

Getting started

์ด๋ฒˆ ์ฑ•ํ„ฐ์˜ ์‹œ์ž‘ ํ”„๋กœ์ ํŠธ ์ด๋ฆ„์€ Testing์ด๋ฉฐ, ์ž…๋ ฅํ•œ hex ์ƒ‰์ƒ ์ฝ”๋“œ์— ๋Œ€ํ•ด์„œ ๊ฐ€๋Šฅํ•œ, RGB๊ฐ’๊ณผ ์ƒ‰์ƒ ์ด๋ฆ„์„ ์ œ๊ณตํ•˜๋Š” ํŽธ๋ฆฌํ•œ App์ด ํฌํ•จ๋˜์–ด ์žˆ๋‹ค. Pod ์„ค์น˜๋ฅผ ์‹คํ–‰ํ•œ ํ›„ ํ”„๋กœ์ ํŠธ workspace์„ ์—ด๊ณ  ์‹คํ–‰ํ•ด๋ณด์ž. ์•ฑ์ด rayWenderrich Green์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์ง€๋งŒ, ์ž„์˜์˜ hex ์ƒ‰์ƒ ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜๊ณ  RGB์™€ ์ด๋ฆ„์„ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

image

ํ•ด๋‹น 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"์—์„œ ๋” ๋งŽ์€ ๋ทฐ ๋ชจ๋ธ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•  ์˜ˆ์ •์ด๋ฉฐ ๊ทธ ๋•Œ ๊ฐ€์„œ ๋‹ค์‹œ ๋งŒ๋‚˜๋ณด์ž

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions