VisasQ Dev Blog

ビザスク開発ブログ

Vue Composition API とユニットテスト

ビザスクアドバイザー開発チーム、フロントエンドエンジニアの山元(@yamagen0915)です。

弊社では新しく開発する画面やリニューアルを行う画面において積極的に Composition API を採用しています。
この記事では Composition API を利用しているコンポーネントでどのようなテストを書いているかを紹介いたします。

Composition API の導入に至った経緯は弊社フロントエンドエンジニアの小柳の記事(ビザスクが Vue Composition API を導入するモチベーション )も是非御覧ください。

前提

弊社ではバックエンド、フロントエンドがはっきり分かれているというよりも、バックエンドもフロントエンドも両方開発しているというエンジニアが多く
Vue に対する知識量にも差があり、これまでテストがあまり書かれていなかったという状況でした。

そのためいきなり完璧なテストを書くのではなく、できるだけ簡単かつ、それなりに重要な部分を優先してテストを書きたいというモチベーションがありました。

何に注目してテストを書くか

一般的にテストとは何かしらの入力に対して、期待する出力が何かを定義するものですが、フロントエンドのテストにおいては 3 種類の入力と出力があると考えています。

  • ユーザーのアクション
    • 入力 : クリックやキーボード入力など
    • 出力 : State の変化、イベントの発火
  • コンポーネントの振る舞い
    • 入力 : Props, State
    • 出力 : 表示の変化など
  • 単純な関数の呼び出し
    • Vue に直接関係のない Util 関数など
    • 入力 : 引数
    • 出力 : 戻り値

この中でも「ユーザーのアクション」はいわゆるビジネスロジックなどの複雑な処理、状態を扱うことも多いため、テストを書くメリットが特に大きいです。

Options API ではこれらはクリックなどを再現してテストする必要があり、そのために vue-test-utils 、js-dom などの周辺ツールの知識も必要であったり、そもそも目的の状態にするためにテスト内で複雑な手順を再現しなければならずハードルが高いという問題がありました。

しかし Composition API では状態やロジックを関数として切り出すことが容易になり、状態を外から注入するなど工夫することでテストが非常に書きやすくなりました。
そのため弊社では特にこの「ユーザーのアクション」に注目してテストを行っています。

ユーザーのアクションのテスト

ありきたりですが TODO リストコンポーネントの追加、削除の機能をそれぞれ add、remove として定義している useTodoList を例に紹介します。

import { reactive } from '@vue/composition-api';

type TodoItem = {
  title: string;
  body: string;
};

export const useTodoList = () => {
  const state = reactive({
    todoList: [] as TodoItem[],
  });

  const add = (todo: TodoItem) => {
    state.todoList.push(todo);
  };

  const remove = (index: number) => {
    state.todoList.splice(index, 1);
  };

  return {
    state,
    add,
    remove,
  };
};

useTodoList は reactive を使っていることを除けば単純な JS のコードのみで記述されているため、下記の通りクリックやテキストボックスへの入力などを再現する必要なく、単純に add、remove を呼び出し state の変更を比較することでテストを行うことができます。

import { createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';

import { useTodoList } from './TodoList';

const localVue = createLocalVue();
localVue.use(VueCompositionAPI);

describe('useTodoList', () => {
  it('TodoItem が追加されること', () => {
    const todoList = useTodoList();
    todoList.add({
      title: 'todo title 0',
      body: 'todo body 0',
    });
    expect(todoList.state.todoList).toEqual([
      {
        title: 'todo title 0',
        body: 'todo body 0',
      },
    ]);
  });

  it('TodoItem が削除できること', () => {
    const todoList = useTodoList();
    todoList.state.todoList = [
      {
        title: 'test title 0',
        body: 'test body 0',
      },
      {
        title: 'test title 1',
        body: 'test body 1',
      },
    ];

    todoList.remove(1);

    expect(todoList.state.todoList).toEqual([
      {
        title: 'test title 0',
        body: 'test body 0',
      },
    ]);
  });
});

コンポーネントの振る舞いのテスト

次に useTodoList を利用して Todo の一覧を表示しているコンポーネントを例に紹介します。

<template>
  <div>
    <button @click="add({ title: 'title', body: 'body' })">追加</button>
    <div v-if="message != null" id="message">
      {{ message }}
    </div>
    <ul>
      <li v-for="(todo, i) in state.todoList" :key="todo.title">
        <h1>{{ todo.title }}</h1>
        <p>{{ todo.body }}</p>
        <button @click="remove(i)">削除</button>
      </li>
    </ul>
  </div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';

import { useTodoList } from './TodoList';

export default defineComponent({
  name: 'TodoList',
  props: {
    message: {
      type: String,
      required: false,
    },
  },
  setup() {
    return useTodoList();
  },
});
</script>

useTodoList にて、追加、削除のテストは行っているため、このコンポーネントのテストとしては Props, State による表示の変化を中心にテストしています。

  • props.message が表示されているか
  • state.todoList と同件数の todo が表示されているか
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import flushPromises from 'flush-promises';

import { useTodoList } from './TodoList';
import TodoList from './TodoList.vue';

const localVue = createLocalVue();
localVue.use(VueCompositionAPI);

describe('TodoList', () => {
  let wrapper: ReturnType<typeof shallowMount>;

  beforeEach(() => {
    wrapper = shallowMount(TodoList, {
      localVue,
      setup() {
        const todoList = useTodoList();
        todoList.state.todoList = [
          {
            title: 'todo title 0',
            body: 'todo body 0',
          },
          {
            title: 'todo title 1',
            body: 'todo body 2',
          },
        ];
        return todoList;
      },
    });
  });

  it('メッセージが表示されること', async () => {
    wrapper.setProps({
      message: 'hello world',
    });
    await flushPromises();
    expect(wrapper.find('#message').text()).toBe('hello world');
  });

  it('Todo の一覧が表示されていること', () => {
    expect(wrapper.findAll('li')).toHaveLength(2);
  });
});

テストできていないこと

上記のテストでは「追加ボタンを押したときに add 関数が呼ばれるか」はテストできていません。
しかし完璧なテストケースを用意することは難しいということと、弊社ではエンジニアによる動作確認はもちろん QA チームによる自動テストなども行われているため、よりロジック部分のテストを優先してテストを書いたほうがよいと考えています。
QA チームの業務については QA チームの綱取の記事(ビザスクのQA業務を紹介)も合わせてご覧ください。

まとめ

Composition API によってロジックをシンプルな関数として切り出すことができるようになり、最低限の知識と最低限の時間で記述できるテストの幅が広がったと感じています。
フロントエンドのテストは完璧なテストケースを用意することが難しいため、最初から完璧なテストを書こうとせず、簡単かつ重要な部分のテストから小さく始めるのもいいのではないでしょうか。