Vue composition-api の テスタビリティを考える


はじめに

Vue composition-api を触っている人も多くいると思うが、これの利用方法についてテスト容易性の観点から考える。

View と ロジックを分離するからテストしやすい

単一ファイルコンポーネント (SFC) には、コンポーネントの表示に関するTemplete (HTML) と CSS そして、ロジックに関する Script (Javascript) が含まれている。
これは、興味を1ファイルに閉じ込めることができるため、直感的にわかりやすく、好きな人が多いと思う。

その一方で、ロジック部分をテストするだけでも vue-test-utils でコンポーネントをマウントするか、this コンテキストを明示して method をコールする必要があった。

import options from "./test-target.vue"

/**
 * this.count++ するようなメソッドの試験
 */
const state1 = {
  count: 1
}
options.methods.increment.bind(state1).call()

厳密には、メソッドなどのロジックは別ファイルに分けることができたが、リアクティブな値は this から値を参照する必要があった。

一方で composition-api はリアクティブな値を含めてコンポーネントと独立してロジックを記述することができる。

import { ref, computed } from "@vue/composition-api"
export default (initValue = 0)=> {
  const _count = ref(initValue)
  const count = computed(()=> _count)
  const increment = ()=> _count.value++
  return { count, increment }
}

テストもしやすい

テスト例
import composition from "./composition"
describe("test", ()=> {
   test("increment", ()=> {
      const {count, increment} = composition(100)
      expect(count.value).toBe(100)
      increment()
      expect(count.value).toBe(101)
   })
})

テスト容易性を高めるための提案

ここまでを踏まえて、テスト容易性を高める提案を2つする。

1. Vue コンポーネントでは依存性の注入のみを行う

composition は setup メソッドで実行して、そこからコンポーネントで使う値を return するが、この setup メソッドには原則としてロジックを書かないことにする。
これによって、もともとテストしにくい Vue コンポーネントにロジックを書かないようになる。
更に依存関係をコンポーネントで解決し、必要な値は composition に対して引数として注入するようになる。
これによってさらに composition のテスト容易性が高まる。

<template>
  <div>Hoge</div>
</template>
<script>
import { inject } from "@vue/composition-api"
import composition from "./composition"
export default {
  props: {
    foo: {
      type: String
    }
  },
  setup(props, { emit }){
     const state = inject('state')
     // props, inject や emit などを解決して注入
     return composition(props, emit, state)
  }
}
</script>

テストをするときは、モックオブジェクトなどを注入すればいい

テスト例
import composition from "./composition"
describe("test", ()=> {
   test("composition", ()=> {
      const mockProps = { foo: "foo" }
      const mockEmit = jest.fn()
      const mockState = { state: "value" }
      const cmp = composition(mockProps, mockEmit, mockState)
      // テストの記述...
   })
})

2. ライフサイクル・ウォッチャー 登録関数も注入する

ライフサイクル登録関数は @vue/composition-api から公開されている。
だが、ライフサイクル登録関数は setup 内で実行される必要があるため、これを composition で使用すると composition 単体で試験するのが難しくなる。

なので、あえて Vue コンポーネントから注入する。

composition例
export default (onMounted)=> {
  const count = ref(0)
  // initialize
  onMounted(()=> count.value = 1)
  return { count }
}

テストするときはモックを注入する

test例
import composition from "./composition"
describe("test", ()=> {
   test("onMounted", ()=> {
      const onMounted = jest.fn()
      const { count } = composition(onMounted)
      expect(count.value).toBe(0)
      onMounted.mock.calls[0][0]()
      expect(count.value).toBe(1)
   })
})

ウォッチャー登録関数はライフサイクル登録関数と違い、 setup で実行する必要はないが、コールバックの処理についてはテストが難しくなることが多いので、登録関数を外から注入する。

composition例
export default (watch)=> {
  const count = ref(0)
  const count2 = ref(0)
  const stopWatch = watch(()=> count.value, (newVal)=> {
    count2.value = newVal * 2
    stopWatch()
  })
  const increment = ()=> count.value++
  return { increment, count, count2 }
}

watch 停止関数を含めてモックする

test例
import composition from "./composition"
describe("test", ()=> {
   test("watch", ()=> {
      const watchMock = jest.fn()
      const watchStopMock = jest.fn()
      watchMock.mockReturnValue(watchStopMock);
      const { count, count2 } = composition(watchMock)
      expect(count2.value).toBe(0)
      // watch コールバックを実行
      watchMock.mock.calls[0][1](3)
      // 実行内容の検証
      expect(watchStopMock).toHaveBeenCalled();
      expect(count2.value).toBe(6)
   })
})

ところで Component の試験はどうするの?

そもそも、template に宣言的に記述してあるイベントハンドラやディレクティブに対してどれほど試験が必要でしょう?
レビューだけでは不十分でしょうか?

仮に、テンプレートの内部に値の変換などのロジックが含まれているようならそれを composition に移すべきです。

その上で、表示に対しての検証が必要であれば、それは Storybook などで確認すれば良いと思います。
変更の検知が必要なら、snapshot テストなどをすれば良いと思います。

テストツールとしての Storybook の利用については以下に考えを記述しています。
https://qiita.com/sterashima78/items/8db32368289e4859480b

更にその先の試験となれば Cypress などで E2E の試験をすることになると思います。

まとめ

composition のテスト容易性を高めたければ、 reactive value の生成とその変更関数のみを composition 内で実装し、それ以外は外から注入するようにしよう。