例によって一筋縄ではいかなかったので共有。
#ライフサイクルフックとは?
公式ドキュメントは以下
Instance Lifecycle Hooks
#どうやってテストする?
基本は普通のコードとまったく同じ。
- 【準備】
- 【実行】
- 【検証】
が、今回の場合やっかいなのは【実行】。
created
や mounted
であれば、vue-test-utils
でVueコンポーネントをマウントした直後に【検証】すれば何となく事足りてしまうが、beforeUpdate
や updated
だとそうはいかない。
記述したライフサイクルフックのロジックを見つけ出し、明示的に実行してやる必要がある。
#ライフサイクルフックは何処?
通常のインスタンスメソッドと同じように記述してあるため、インスタンス直下で簡単に見つかりそうなものだがさにあらず。
試しにwrapper.vm
の下をいくら探しても見つからない。
ならばとGitHub上で検索してみると、何やらそれらしいモノが見つかる。
beforeUpdate?(): void;
updated?(): void;
これは要するに、
-
$options
プロパティの下に -
beforeUpdate
やupdated
が -
undefined
若しくは 引数無し・戻り値無しの関数 として存在
という風に読める。
#テスト(その1)~ライフサイクルフック関数の存在確認~
試しに、Vueコンポーネントに何らかライフサイクルフックを記述し、そのライフサイクルフック関数の存在確認テストを書いてみる。
@Component
export default class MyComponent extends Vue {
/* 中略 */
/* lifecycle hooks */
private beforeUpdate() {
/* まだ空っぽで構わない */
}
describe('lifecycle hooks', () => {
beforeEach(() => {
wrapper = shallowMount(MyComponent);
});
describe('beforeUpdate', () => {
it('should be function', () => {
expect(wrapper.vm.$options.beforeUpdate).toBeInstanceOf(Function);
});
});
});
expect(value).toBeInstanceOf(constructor)
Expected constructor: Function
Received constructor: Array
Received value: [[Function beforeUpdate]]
xx |
xx | it('', () => {
> xx | expect(wrapper.vm.$options.beforeUpdate).toBeInstanceOf(Function);
| ^
xx | });
xx |
ん? エラー!?
Function
ではなく、Function beforeUpdate
の配列とな!?
詳細を調べるため、ターミナルからデバッグを起動。
node_modules\@vue\cli-plugin-unit-jest\README.md
には runInBand
オプション無しのコマンドが載ってますが、並列で走られたら追っ掛けられる訳ないんでご注意を。
# macOS or linux
> node --inspect-brk ./node_modules/.bin/vue-cli-service test:unit --runInBand
# Windows
> node --inspect-brk ./node_modules/@vue/cli-service/bin/vue-cli-service.js test:unit --runInBand
chrome://inspect
を開いて適当にやってれば何とかなるなる。
ブレークポイントを設定し、配列の先頭要素 wrapper.vm.$options.beforeUpdate[0]
の内容を .toString()
等で確認してみると、Vueファイルに書いた beforeUpdate
で間違いなさそう。
▼Watch
wrapper.vm.$options.beforeUpdate[0]: "beforeUpdate() {}"
そんな訳で、どうやら vuejs/vue/types/options.d.ts
内の定義が実態と合ってないっぽい。
とはいえ、関数の配列と分かっていれば後はそれを参照できさえすれば良い。
#テスト(その2)~ライフサイクルフック関数を参照する~
まずは配列の先頭要素を、添字を使って参照してみる。
なお、変数名 handlers
は node_modules/vue/src/core/instance/lifecycle.js
に倣った。
describe('lifecycle hooks', () => {
let handlers: (() => void)[] | undefined;
beforeEach(() => {
wrapper = shallowMount(MyComponent);
handlers = wrapper.vm.$options.beforeUpdate;
});
it('should be function', () => {
if (handlers && handlers.length > 0) {
expect(handlers[0]).toBeInstanceOf(Function);
}
});
});
まぁ、型が合わないのでいきなりエラー発生。
型 '(() => void) | undefined' を型 '(() => void)[] | undefined' に割り当てることはできません。
Type '() => void' is missing the following properties from type '(() => void)[]': pop, push, concat, join, and 27 more.ts(2322)
いずれ直ることを期待しつつ、所詮はテストコードなので当座 as
句で凌いでおく。
describe('lifecycle hooks', () => {
let handlers: (() => void)[] | undefined;
beforeEach(() => {
wrapper = shallowMount(MyComponent);
handlers = wrapper.vm.$options.beforeUpdate as (() => void)[] | undefined;
});
it('should be function', () => {
if (handlers.length > 0) {
expect(handlers[0]).toBeInstanceOf(Function);
}
});
});
これで記述したライフサイクルフック関数を無事参照できるようになった。
#テスト(その3)~ this コンテキストの扱い~
最後に this
コンテキストの扱いについて記載しておく。
公式のドキュメント Options / Lifecycle Hooks の注意書きにもある通り、
ライフサイクルフック関数内では this
を自動的に Vueコンポーネントのインスタンスとして設定してくれる。(そのため、ライフサイクルメソッドをアロー関数で記述するべきではない)
しかし、テストコード内ではそういうフレームワークによる作用は無い。自分の手で明示的に this
コンテキストを指定しなければならない。
テストコード内で Vueコンポーネントのインスタンスを指すのはもちろん wrapper.vm
なので、call
や apply
を用いて指定してやればOK。
describe('lifecycle hooks', () => {
let handlers: (() => void)[] | undefined;
beforeEach(() => {
wrapper = shallowMount(MyComponent);
handlers = wrapper.vm.$options.beforeUpdate as (() => void)[] | undefined;
});
it('should update instance property X', () => {
if (handlers && handlers.length > 0) {
/* 準備 */
wrapper.setData({ myProp: false });
/* 実行(thisコンテキスト指定) */
handlers[0].call(wrapper.vm);
/* 検証 */
expect(wrapper.vm.$data.myProp).toBe(true);
}
});
});
以上で、ライフサイクルフックのテストを自在に書けるはず。
めでたしめでたし。