永遠にWIP

requestAnimationFrameを使ったカスタムhookのテストを書く

Frontend
React

はじめに

requestAnimationFrameを使ったカスタムhookを作成したときに、どうテストすればいいのかわからなかったので、メモとして残しておきます。

既存の実装を見に行く

まずは react-use の実装を見に行くことにしました。react-useは便利なカスタムhookをまとめているライブラリなので参考になると思いここを見に行きました。
実際にrequestAnimationFrameを使っている箇所を調べたろころraf-stubというライブラリを使いテストをしていることをしました。

https://github.com/alexreardon/raf-stub

raf-stubでは、requestAnimationFrameで実行すべき関数をraf-stub側に保存し、stepという関数を実行したタイミング実行するような実装になっていました。

実際に実装する

先程挙げたraf-stubを使ってもいいのですが、raf-stubは型がなくDefinitelyTypedにも登録されていません。また、一つのカスタムhookのためにライブラリを導入するのもどうかなと思ったためraf-stubを参考に簡単に自分で実装しました。

class RafStub {
  private index = 0
  private time = 0
  private que: { fn: FrameRequestCallback; index: number }[] = []

  private duration: number = 1000 / 60; 

  public requestAnimationFrame(fn: FrameRequestCallback) {
    this.que.push({ fn, index: this.index })
    this.index += 1
    return this.index
  }

  public step() {
    const q = this.que.shift()
    this.time += this.duration
    q.fn(this.time)
  }
}

本当に最小構成で、requestAnimationFrame は window.requestAnimationFrame を置き換えるときに使うメソッドで、実際にrequestAnimationFrameで実行すべきだった関数を呼び出すときは step メソッドを呼び出すという形になっています。

このRafStubを使って実際にテストコードを書いてみます。
今回は以下のようなカスタムhookをテストしてみます。

const useCounter = () => {
  const [count, setCount] = useState(0)
  
  const increment = () => {
    setCount(cur => cur + 1)
    requestAnimationFrame(increment)
  }

  useEffect(() => {
    requestAnimationFrame(increment)
  }, [])

  return count
}

ひたすらincrementという関数を呼び、countを増加させていく簡単なhookになります。
これのテストを以下に実装してみます。

import { renderHook, act } from '@testing-library/react-hooks/dom'

describe('useCount' , () => {
  let rafStub: RafStub | null

  beforeEach(() => {
    rafStub = new RafStub()
    global.window.requestAnimationFrame = rafStub.requestAnimationFrame.bind(rafStub)
  })

  afterEach(() => {
    rafStub = null
  })

  it('countが増加する', () => {
    const { result } = renderHook(() =>
      useCounter()
    )
    expect(result.current).toBe(0)

    act(() => {
      rafStub.step()
    })
    expect(result.current).toBe(1)

    act(() => {
      rafStub.step()
    })
    expect(result.current).toBe(2)
  })
})

上の通り、rafStub.step() を実行するたびにcountが1ずつ増えているのがテストできています。

おわりに

最近テストを書く機会がなかったのですが、テストを書くとやはり安心してリファクタリングできるし、心理的安全性が確保されていて非常に良いですね。

© 2020 DuGlaser