6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Vue】ライフサイクルフックのテストを書く【TypeScript】

Last updated at Posted at 2019-05-20

例によって一筋縄ではいかなかったので共有。
#ライフサイクルフックとは?
公式ドキュメントは以下
Instance Lifecycle Hooks

#どうやってテストする?
基本は普通のコードとまったく同じ。

  • 【準備】
  • 【実行】
  • 【検証】

が、今回の場合やっかいなのは【実行】。
createdmounted であれば、vue-test-utilsでVueコンポーネントをマウントした直後に【検証】すれば何となく事足りてしまうが、beforeUpdateupdated だとそうはいかない。
記述したライフサイクルフックのロジックを見つけ出し、明示的に実行してやる必要がある。

#ライフサイクルフックは何処?
通常のインスタンスメソッドと同じように記述してあるため、インスタンス直下で簡単に見つかりそうなものだがさにあらず。
試しにwrapper.vmの下をいくら探しても見つからない。

ならばとGitHub上で検索してみると、何やらそれらしいモノが見つかる。

vuejs/vue/types/options.d.ts(抜粋)
  beforeUpdate?(): void;
  updated?(): void;

これは要するに、

  • $optionsプロパティの下に
  • beforeUpdateupdated
  • undefined 若しくは 引数無し・戻り値無しの関数 として存在

という風に読める。

#テスト(その1)~ライフサイクルフック関数の存在確認~
試しに、Vueコンポーネントに何らかライフサイクルフックを記述し、そのライフサイクルフック関数の存在確認テストを書いてみる。

任意の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で確認
▼Watch
wrapper.vm.$options.beforeUpdate[0]: "beforeUpdate() {}"

そんな訳で、どうやら vuejs/vue/types/options.d.ts 内の定義が実態と合ってないっぽい。
とはいえ、関数の配列と分かっていれば後はそれを参照できさえすれば良い。

#テスト(その2)~ライフサイクルフック関数を参照する~
まずは配列の先頭要素を、添字を使って参照してみる。
なお、変数名 handlersnode_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);
      }
    });
  });

まぁ、型が合わないのでいきなりエラー発生。

VSCode上でのエラー
型 '(() => 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 なので、callapply を用いて指定してやればOK。

テストコード(thisコンテキスト指定)
  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);
      }
    });
  });

以上で、ライフサイクルフックのテストを自在に書けるはず。
めでたしめでたし。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?