Todo アプリを firebase にデプロイしたがテストの書き方がわからなかった

ここ最近はブログの交信がさぼり気味だった。というのも、realtime database のテストの仕方がわからずに試行錯誤していたので、成果をあまりだせなかったというのが大きい。

fetch-mock からの脱却

ローカル環境のデータベースへアクセスしていたときは、データベースへのアクセスをREST API経由で行っていたので、クライアント側はfetch関数を使用していたが、realtime database は専用のクライアントAPIによってアクセスするためfetch関数は使えなくなった。それに伴って、fetch-mockを使用してテストを行っていた部分が壊れてしまったので、realtime database 用のモックが必要となった。調べてみると realtime database シミュレーターなるものがあったわけだが、これはデータベースのルールに関してテストするもので、かつ、まだベータ版とのことで使用は控えたほうがよさそうとの結論になった。

結局は jest のモックを使用して firebase.database を偽装することにした。

jest.fn()

jestはjavascript用のテストフレームワークでfecebookが主に開発を行っている模様。fetch-mock を使っているときは sinon というテストフレームワークを使用していたが、Reactを使う上では jest のほうが親和性が高いかな? と思ったので jest に切り替えた。

jestではモックを作成するのに jest.fn() を使う。内部で呼び出された関数に差し替えることで関数の呼び出しを偽装できる。実際には jest.fn().mockImplementation() によって関数を実装することで偽装する。

firebase.database() を偽装する

firebase.database() は sendToApiServer 内部で呼び出しを行っているので、現状のままでは簡単にモック関数と差し替えることはできない。なので、sendToApiServer 関数をやめて、DatabaseBridgeという名前のクラスを作成し、そいつに処理を移した。そして、getDatabase() という関数を作成し、const database = firebase.database() となっている部分を const database = getDatabse() にしたうえで、getDatabase() 関数の中で return firebase.database() として外向けに公開することでテストでも差し替え可能な関数でありつつ、内部処理の影響を最小限にした。

モック関数をネストする

firebase.database() をモック化することには成功したが、そのあと database の関数を使用している処理もモック化しないと、その部分でコケてしまう。具体的には database.ref() や そのあとの ref.on() や ref.once() でコケる。それらを mockImplementation() を組み合わせて作成する。

const bridge = new DatabaseBridge()
const mock = jest.spyOn(bridge, 'getDatabase'); // spyOn() は 第一引数で渡したインスタンスの中の第二引数のメソッドを監視する

const mockDbRemove = jest.fn() // ref.remove() メソッドのモック
const mockDbRef = jest.fn() // database.ref() メソッドのモック
mockDbRef.mockImplementation(() => { // database.ref() メソッドを偽装する
  return { remove: mockDbRemove } } // ネストさせることで内部の関数もモック化する
})
mock.mockImplementation(() => { // getDatabase() メソッドを偽装する
  return { ref: mockDbRef } // database.ref() のモック関数を埋め込み
})

例えば、実装が以下のようになっている場合、getDatabase() で取得したデータベースオブジェクトはモックにさし変わっているので、その次の database.ref() はモックオブジェクトが持つメソッドが呼ばれることになる。同様にその次の ref.remove() もモックオブジェクトのメソッドが呼ばれる。

class DatabaseBridge {
  // 実際は firebase.database が使用されるが、テストでは mock変数に差し替えられる
  getDatabase() {
    return firebase.database();
  }

  remove() {
    const database:any = getDatabaase() // ここでモック(mock)が返される
    const ref = database.ref(`...`); // ネストしたモック(mockDbRef)が実行される
    ref.remove() // mockDbRemove モックが実行される
  }
}

モックオブジェクトのコール情報を確認する

偽装しただけではテストできないので、その時に渡された引数をチェックするテストを書く必要がある。

  • expect(mock.mock.calls.length).toBe(???)

何回モックがコールされたかをチェックする。

  • expect(mock.mock.calls[0][0]).toEqual(???)

コールされたときの引数の値が期待通りかをチェックする。

今のところ、この二つでまかなえているので、それ以外は今後覚えていく。

jest.fn().mockImplementationOnce()

モック関数のコールに対して一度だけ違う値を返したいみたいな場合は、mockImplementationOnce() で実装できる。

const mockDbOnce = jest
  .fn()
  .mockImplementationOnce(() => {
    return false
  })
  .mockImplementationOnce(() => {
    return true
  })

このモックが呼ばれると、一回目はfalseを返すけど、二回目はtrueを返すようになる。

成果物

GitHub - bamchoh/react-study at 7c28892ea0cac3b23af46d18be7968fbdc86b2db