LoginSignup
15
13

More than 5 years have passed since last update.

redux-observableでActionsObservableを利用して簡易にテストを書く

Posted at

redux-observableのテストとして、mock-storeを利用したテストやmarble testingが挙げられる。
mock-storeを利用したテストは非同期で小面倒くさかったり、storeの中身のテストになっていてepicのテストとして扱いづらい。
marble testingはスマートだけどRx慣れしてない者にとってはちょっと難易度が高い。

ActionsObservableを使って小さめのテストする。

今回はActionsObservableを利用したテストを紹介する。
ActionsObservableはundocumentedではあるが、index.d.ts などで定義されていたり、marble testingのサンプルとして利用されている。
テストで利用する分には問題ない気がするが、それ以上に使う時は注意が必要かもしれない。

今回はmochaを利用している前提でサンプルコードを書いていく

ActionsObservableでepicだけテスト

とりあえずこんなepicを用意する

require("rxjs")
const pingEpic = (action$) => {
  return action$.ofType("PING")
    .map( () => {
      return { type: "PONG" }
    })
}

そしてこんな感じでテストが書ける

const assert = require("assert")
const { ActionsObservable } = require("redux-observable")

describe("epic test", () => {
  it("ping epic", (done) => {
    // `ActionObservable`は`Observable`の拡張なので、`of`など使える。
    const mockAction = ActionsObservable.of({type: "PING"})
    pingEpic(mockAction)
      .toArray() // assertしやすいように`toArray`する
      .subscribe( result => { // subscribeで受取り
        assert.deepEqual(result, [
          {type: "PONG"} // こういうactionが来るハズ
        ])
        done()
      })
  })
})

Actionをmockするだけでその結果に来るはずのactionをテストすることが可能になった。嬉しい。

また、複数のactionを飛ばせば複数のactionがsubscribe出来る。

  it("ping epic", (done) => {
    const mockAction = ActionsObservable.of(
      {type: "PING"},
      {type: "PING"}
    )
    pingEpic(mockAction)
      .toArray()
      .subscribe( result => {
        assert.deepEqual(result, [
          {type: "PONG"},
          {type: "PONG"}
        ])
        done()
      })
  })

combineEpicsと組み合わせる

combineEpicsを使えば複数のepicがある前提のテストも出来る

require("rxjs")
const pingEpic = (action$) => {
  return action$.ofType("PING")
    .map( () => {
      return { type: "PONG" }
    })
}

const anotherPingEpic = (action$, store) => {
  return action$.ofType("PING")
    .map( () => {
      return { type: "PUNG", payload: "foo"}
    })
}
const assert = require("assert")
const { ActionsObservable, combineEpics } = require("redux-observable")

describe("epic test", () => {
  it("ping epic ( with combine )", (done) => {
    const mockAction = ActionsObservable.of({type: "PING"})
    const combinedEpic = combineEpics(
      pingEpic,
      anotherPingEpic
    )
    combinedEpic(mockAction)
      .toArray()
      .subscribe( result => {
        // 一つのactionに対して複数のepicからの処理が返る
        assert.deepEqual(result, [
          {type: "PONG"}, // pingEpicが吐き出したやつ
          {type: "PUNG", "payload": "foo"}, // anotherPingEpicが吐き出したやつ
        ])
        done()
      })
  })
})

何らか複数のepicが絡む場合のテストがしたいなら使えるだろう

非同期扱う

Promiseを利用した非同期も問題なく扱える

// lazyPing
const lazyPing = () => {
  return new Promise( res => {
    setTimeout( () => {
      res("DELAY")
    }, 100)
  })
}
// epic.js
require("rxjs")
const pingEpic = (action$) => {
  return action$.ofType("PING")
    .mergeMap( () => lazyPing() ) // promiseはmergeMapなどで処理出来る
    .map( (result) => {
      return { type: "PONG", payload: result}
    })
}
// test.js
const assert = require("assert")
const { ActionsObservable, combineEpics } = require("redux-observable")

describe.only("epic test", () => {
  it("lazy ping epic", (done) => {
    const mockAction = ActionsObservable.of({type: "PING"})
    pingEpic(mockAction)
      .toArray()
      .subscribe( result => {
        assert.deepEqual(result, [
          {type: "PONG", payload: "DELAY"}
        ])
        done()
      })
  })
})

storeをmock化して使う

storeも使って何かしらやったりするのも出来る

// epic.js
require("rxjs")

const counterEpic = (action$, store) => {
  return action$.ofType("DO_INCREMENT")
    .map( (action) => {
      return { 
        type: "INCREMENT", 
        payload: action.payload + store.getState().baseCount
      }
    })
}

// test.js
const assert = require("assert")
const { ActionsObservable, combineEpics } = require("redux-observable")

describe("epic test", () => {
  it("counter epic", (done) => {
    // actionと一緒にstoreもmockする
    const mockAction = ActionsObservable.of({type: "DO_INCREMENT", payload: 2})
    const mockStore = {
      getState() {
        return { baseCount: 10 }
      }
    }
    counterEpic(mockAction, mockStore)
      .toArray()
      .subscribe( result => {
        assert.deepEqual(result, [
          {type: "INCREMENT", payload: 12},
        ])
        done()
      })
  })
})

まとめ

  • ある程度簡易なテストはActionsObservableだけで出来た。
  • storeのmockから解放されるのは結構嬉しい
    • reducerまで絡んでくるとかになるまでは使わなくて良さそう
  • タイミングなどもっとシビアにやりたいならmarble testingを導入してくのが良さそう
15
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
13