redux-observableのcreateEpicMiddlewareでDIを活用する

redux-observableでcreateEpicMiddleware時に依存するAPIを第2引数に渡すとテストが簡易になります。


以下のブログを参考にまずは、jest.mockを使うコードを書いていきます。

[redux-observable の処理を marble testing で簡単にテストする](https://jyane.jp/2016/12/26/redux-observable.html)

本来は別々のファイルに書きますが、今回はとりあえずcreate-react-appのindex.jsに以下を貼り貼り付けて準備完了です。
ブラウザで動かすにはコメントアウトしてください。

```index.js
import 'rxjs'
import ReactDOM from 'react-dom'
import * as React from 'react'
import { createStore, applyMiddleware } from 'redux'
import { Provider, connect } from 'react-redux'
import { createEpicMiddleware } from 'redux-observable'
import { Observable } from 'rxjs/Observable'
import { API } from './api'

export const FETCH_USER_REQUEST = 'FETCH_USER_REQUEST'
export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS'
export const FETCH_USER_FAILURE = 'FETCH_USER_FAILURE'

const fetchUsere = () => ({ type: FETCH_USER_REQUEST })
const fetchUserSuccess = data => ({ type: FETCH_USER_SUCCESS, payload: data })
const fetchUserFailure = err => ({ type: FETCH_USER_FAILURE, err })

export const epic = action$ =>
  action$.ofType(FETCH_USER_REQUEST).mergeMap(action => {
    return API.getUser()
      .map(v => fetchUserSuccess(v.data))
      .catch(err => fetchUserFailure(err))
  })

const userReducer = (state = { user: { name: '' } }, action) => {
  switch (action.type) {
    case FETCH_USER_SUCCESS:
      return { user: action.payload }
    default:
      return state
  }
}

let App = ({ user, fetchUsere }) => (
  <div>
    <h1>{user.name}</h1>
    <button onClick={fetchUsere}>fetchUsere</button>
  </div>
)

App = connect(({ user }) => ({ user }), { fetchUsere })(App)

const epicMiddleware = createEpicMiddleware(epic)

const store = createStore(userReducer, applyMiddleware(epicMiddleware))

// ReactDOM.render(
//   <Provider store={store}>
//     <App />
//   </Provider>,
//   document.getElementById('root')
// )
```

APIはクラスで書いていくと思いますが、簡単のためオブジェクトで代用します。

```api.js
import { Observable } from 'rxjs/Observable'

export const API = {
  getUser() {
    return Observable.of({ data: { name: 'キズナアイ', age: 33 } })
  }
}
```

さて、jest.mockを活用したテストは以下のようになります。

```index.test.js
import { ActionsObservable } from 'redux-observable'
import { TestScheduler } from 'rxjs'

let cold
let testScheduler

beforeEach(() => {
  testScheduler = new TestScheduler((expected, actual) => {
    expect(expected).toEqual(actual)
  })
  cold = testScheduler.createColdObservable.bind(testScheduler)
})

afterEach(() => {
  testScheduler.flush()
})

test('APIの成功時、FETCH_USER_SUCCESSを発行する', () => {
  jest.mock('./api', () => {
    const { Observable } = require('rxjs/Observable')
    return {
      API: {
        getUser: () => Observable.of({ data: { name: '山田' } })
      }
    }
  })
  const { epic } = require('.')
  const values = {
    a: { type: 'FETCH_USER_REQUEST' },
    b: { type: 'FETCH_USER_SUCCESS', payload: { name: '山田' } }
  }

  const input$ = '--a-'
  const expect$ = '--b'

  const test$ = epic(new ActionsObservable(cold(input$, values)), testScheduler)
  testScheduler.expectObservable(test$).toBe(expect$, values)
})
```

jest.mock内にある`require('rxjs/Observable')`からやや危険な感じがします。

これをDI的に書き換えて行きます。

公式のドキュメントではこのページです。

https://redux-observable.js.org/docs/recipes/InjectingDependenciesIntoEpics.html

```index.js

export const epic = (action$, _, { API }) =>
  action$.ofType(FETCH_USER_REQUEST).mergeMap(action => {
    return API.getUser()
      .map(v => fetchUserSuccess(v.data))
      .catch(err => fetchUserFailure(err))
  })


const epicMiddleware = createEpicMiddleware(epic, {
  dependencies: { API }
})
```

createEpicMiddlewareでAPIクライアントをインジェクト出来ました。

するとテスト内で複雑なモックを使わずに、オブジェクトを渡すだけになりました。

```index.test.js
import { TestScheduler } from 'rxjs'
import { Observable } from 'rxjs/Observable'
import { ActionsObservable } from 'redux-observable'
import { epic } from '.'

let cold
let testScheduler

const API = {
  getUser: () => Observable.of({ data: { name: '山田' } })
}

beforeEach(() => {
  testScheduler = new TestScheduler((expected, actual) => {
    expect(expected).toEqual(actual)
  })
  cold = testScheduler.createColdObservable.bind(testScheduler)
})

afterEach(() => {
  testScheduler.flush()
})

test('APIの成功時、FETCH_USER_SUCCESSを発行する', () => {
  const values = {
    a: { type: 'FETCH_USER_REQUEST' },
    b: { type: 'FETCH_USER_SUCCESS', payload: { name: '山田' } }
  }

  const input$ = '--a-'
  const expect$ = '--b'

  const test$ = epic(
    new ActionsObservable(cold(input$, values)),
    testScheduler,
    { API }
  )
  testScheduler.expectObservable(test$).toBe(expect$, values)
})
```

実際には、成功時とエラー時のAPIのオブジェクトを用意して、もう少し抽象した関数を用意してあげるとよりシンプルになると思います。