LoginSignup
13
12

More than 3 years have passed since last update.

Vue 3.0にてcomposition apiを使ったcomposableパターンでテストを書く

Posted at

昔Advent Calendarのために書いた記事をvue-next版に書き直しました。
https://qiita.com/jiko21/items/12c79b7b831276e9a088

TL;DR

  • composition-apiを使うことでロジックをUIから分離してテストできる
  • ただし、少し考えてテストを書く必要あり

はじめに

9/18にVue 3.0がリリースされました!
これにより、composition-apiがプラグイン無しで利用できるようになりました。
composition-apiの詳しい使い方はここでは省略しますが、これにより、コンポーネントからロジックを別ファイルへと分離することもできます。

今回は、コンポーネントからロジックを分離した際のテストの書き方を説明します。

コードはすべてこちらにあります。

ロジックとUIの分離

例えば、TODOアプリの場合は、TODOリストに追加、削除する処理や、TODOリストのデータなどをComponentに直接記述するのではなく、
あくまで別のファイルに記述し、Component側でそれらを呼び出す、といったことがComposition-apiでは可能となります。

@/composable/todo.ts
import { computed, reactive } from 'vue';

const useTodo = () => {
  const todo = reactive({
    todos: [] as string[],
    length: computed(() => todo.todos.length),
  }) as any;
  const addTodo = (item: string) => {
    todo.todos.push(item);
  };
  const deleteTodo = (index: number) => {
    todo.todos.splice(index, 1);
  };
  return {
    todo,
    addTodo,
    deleteTodo,
  };
};

export default useTodo;
Todo.vue
<template>
  <div class="count">
    <input id="todo-input" v-model="text"/>
    <button class="add-btn" @click="onSubmit">追加</button>
    <ul>
      <li v-for="(task, i) in todo.todos" :key="i">
        <p>{{task}}</p>
        <button class="delete-btn" @click="deleteTodo(i)">Delete</button>
      </li>
    </ul>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import useTodo from '@/composable/todo';

export default defineComponent({
  name: 'Todo',
  setup() {
    const text = ref('');
    const { todo, addTodo, deleteTodo } = useTodo(); // 外部ファイルに書いたロジックを読み込む
    ...
  },
});
</script>

<style scoped>
</style>

テストの実装

このようにロジック(composable)とUIを分離したので次はテストの実装方針を説明します。
まずは、ロジック側からですが、

実際にモジュールとして正しく動くか

を検証します。(当たり前っちゃ当たり前かもですが)
そして、UI側では

ボタンタップ時にロジックをcallできているか

を検証します。

ロジック側のテスト

サービス層やutilで書いたテストコードと同様に、実際に呼び出しを行い検証を行います。
(もしcomosableに何らかの依存関係がある場合はそれをmockしてください)

todo.spec.ts
import useTodo from '@/composable/todo';

describe('todo.spec.ts', () => {
  it('addTodo should work properly', () => {
    const { todo, addTodo, deleteTodo } = useTodo();
    addTodo('hogehoge');
    expect(todo.todos).toEqual(['hogehoge']);
  });
  ...
});

UI側のテスト

ロジック(composable)をモックしてやり、それが呼ばれたかを検証します。
compsable内にstateがある場合はそれが表示されるかも確認しておきましょう。

Todo.spec.ts
import { mount, VueWrapper } from '@vue/test-utils';
import Todo from '@/components/Todo.vue';
import * as composable from '@/composable/todo';

describe('Todo.vue', () => {
  let wrapper: VueWrapper<any>;
  let addTodoMock: jest.Mock;
  let deleteTodoMock: jest.Mock;

  beforeEach(() => {
    jest.mock('@/composable/todo');
    addTodoMock = jest.fn();
    deleteTodoMock = jest.fn();
    const TODOS = [
      'アドベントカレンダー',
      '修論',
      '筋トレ',
    ];
    jest.spyOn(composable, 'default').mockReturnValue({
      // Reactiveはデータ構造そのままでOK!
      todo: {
        todos: TODOS,
        length: () => TODOS.length,
      },
      addTodo: addTodoMock,
      deleteTodo: deleteTodoMock,
    });
    wrapper = mount(Todo);
  });

  it('correctly renders initial html', () => {
    expect(wrapper.html()).toMatchSnapshot();
  });

  it('correctly call deleteTodo when `Delete` button is clicked', () => {
    const INDEX = 1;
    wrapper.findAll('.delete-btn')[INDEX].trigger('click');
    expect(deleteTodoMock).toHaveBeenCalledWith(INDEX);
  });
  ...
});

ここで重要なのは、Reactiveのモック方法です。
データ構造そのままのObjectをmockしてやればできます。

これはRefも同様で、

const countValue = ref(0);
countValue.value;

のように使用するので

jest.spyOn(composable, 'default').mockReturnValue({
  countValue: {
    value: 0,
  }, 
  increment: incrementMock,
  decrement: decrementMock,
});

と書いてしまいたくなりますが

count.spec.ts
jest.spyOn(composable, 'default').mockReturnValue({
  countValue: 0 as any,
  increment: incrementMock,
  decrement: decrementMock,
});
wrapper = mount(Count);

のように書いてやる必要があります。

最後に

このようにすれば、composableとしてロジックを分離した場合でもテストがかけます。
ただし、vue-test-utilsが9/28時点でまだ2.0.0-beta.5なのでまだまだ安定しないかもしれません。

13
12
1

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
13
12