概要
Composition APIを用いて作成したVuex的なGlobal Stateを単体テストしようと思って色々調べた結果を小ネタとしてQiitaに共有しておきます。
今回のTipsが反映されたアプリケーションをGitHubにあげています。
ちなみに今回はComposition APIのライブラリはNuxt.jsを使っているので、@vue/composition-api
ではなく、@nuxt/composition-api
を使っていますが、本記事の内容は@vue/composition-api
にも適応できる考え方だと思っています。
またテスト周りのツールはJest + @vue/test-utils
になります。
composition functionを単体テストする
そもそもComposition APIで提供されているcomputed
, watch
, onMounted
などの機能はsetup
コンポーネントオプション内で呼ばれることを想定しており1、@nuxt/composition-api
で提供されるuseContext
も同様の思想で設計されています。
Composition API の使用を開始するには、初めに実際に使用できる場所が必要です。Vue コンポーネントでは、この場所を setup と呼びます。
...
オプション API に比べて Composition API の機能を完全にするには、ライフサイクルフックを setup の中に登録する必要があります。https://v3.ja.vuejs.org/guide/composition-api-introduction.html より引用
なので例えば以下の様なcomposition functionをテストしたい場合は、useCounter
をテストケースから直接呼び出すのではなく、setup
コンポーネントオプション内で呼び出す必要があります。
import { computed, reactive } from "@nuxtjs/composition-api"
interface State {
count: number
}
export function useCounter() {
const state: State = reactive({ count: 0 })
const increment = (state: State) => () => state.count++
return {
count: computed(() => state.count),
increment: increment(state)
}
}
しかし愚直に書くと、テストケースごとにVueコンポーネントのマウント処理を書いていく必要があり、手間はかかるし、テストコードの可読性も下がるし、結構しんどいところはあります。
import { mount } from "@vue/test-utils"
import { useCounter } from "./counter.ts"
describe("Counter", () => {
it("increment", () => {
mount(
Vue.extend({
setup() {
const { count, increment } = useCounter()
increment()
expect(count.value).toBe(1)
return {}
},
template: "<div></div>"
}
)
})
})
なので少し面倒ですが、以下の様なヘルパーを定義することによって比較的スマートにテストコードを書くことができます。
export const createSetupScope = (localVue: typeof Vue, done: jest.DoneCallback, mocks = {}) => {
return (test: () => (unknown | Promise<unknown>)) => {
const stub = Vue.extend({
setup() {
const callable = test as any
const result = callable.constructor.name === "AsyncFunction"
? callable()
: new Promise((resolve, reject) => { try { resolve(callable()) } catch (e) { reject(e) } })
result.then(() => done()).catch((e: any) => done(e))
return {}
},
template: "<div></div>"
})
mount(stub, {
localVue,
mocks
})
}
}
import { createLocalVue } from "@vue/test-utils"
import { useCounter } from "./counter.ts"
import { createSetupScope } from "./testing.ts"
const localVue = createLocalVue()
describe("Counter", () => {
it("increment", done => {
const setup = createSetupScope(localVue, done)
setup(() => {
const { count, increment } = useCounter()
increment()
expect(count.value).toBe(1)
})
})
})
一応の注意点としては俯瞰してみるとexpect
が実行されるのは、setup
コンポーネントオプションの中で実行されるので、通常のテストとは違い非同期でテストが実行されるということです。
つまりit
のコールバックの処理が最後まで到達しても、テストが完了していないことを意味します。
しかし幸いにもJestには非同期で実行されるテストに対応するために、it
のコールバックの第一引数にDoneCallback
が渡されるように設計されています。
非同期で実行されるテストにおいて、ユーザー側でDoneCallback
を任意のタイミングで呼び出すことで、it
の処理が終了したあとでも非同期な処理の実行が完了することを待つことができます。
今回の場合、そういった背景から個人的にはComposition APIを使った実装コードのテストをする場合、it
のコールバックに渡されたDoneCallback
をcreateSetupScope
に渡すようにしていこうかなとは思っています。
-
といいつつも実は
@nuxtjs/composition-api: ^0.24.6
時点では記載されているテストコードはsetup
コンポーネントオプションの中で実行せずとも成功しますので、今後はもしかすると、MVVMでいうところのModelを表現するcomposition functionを作る分には、setup
コンポーネントオプション外で提供されるAPIが実行できる世界が実現するかもしれません。 ↩