jestでAPIなど他リソースが必要な機能のテストをしてみる

# この記事について

wasmモジュールを使用したアプリを作成後、Unitテストを作成しようとした時、wasmモジュールが関係している所でエラーが出て苦労していました。※以前の記事「[jestを使ってVue3のUnitテストをやってみようとして苦労した話](https://qiita.com/etnk/items/94156de78ffb9ffc532a)」

今回、wasmが云々という以前に、そもそもjestで他リソースが必要な時のテストってどうするんだ？という観点で対応したのでその記録になります。その為、記事の前半部分は表題と関係なく思えるかもしれません。表題の内容が早く読みたい方は後半部分の「テストケース実装」から読んで頂く事をお奨めします。

# 前提技術

jest
Vue3

# 状況整理

どのような方向で対応すべきかを考える為に、まず現状を整理します。

## 対象アプリ

※2022年7月30日、ドメイン有効期限切れによりURL更新

https://www.cubetrain.silverbox.link/

## テストケースを作成したい理由

- 操作手順を戻したり、その状態で再度回転操作したりなど、各種状況が存在する
- それぞれの状況におけるそれぞれの機能の動作をちゃんと定義しておきたい
- 何かを修正する事で、他の動作仕様が変わったり不具合を発生したりする事が無い様にしたい

## 発生している問題

- wasmモジュールを読み込んでいるVueコンポーネントがある
- jestが仮想Vueレンダリングをする際にその読み込み部分でエラーが発生する（具体的にはファイルのfetch処理）

# 考察

今回問題になっているwasmモジュール読み込み部分ですが、行いたいテストとは直接関係ありません。言わばAPIコールが出来なくてエラーになっている様なものです。もちろんwasmモジュールの動作もテストしたい所ですが、今回の目的とは別になります。
jestでAPIコールしている部分のテストはノウハウが多くありそうです。それを適用すれば今回の目的は達成できると思われます。

# 情報収集

jestでどんな対応をするべきなのか情報収集してみます。

## axiosを使うケース

APIコールと言えばaxiosです。検索すると、結果のページには「モック」という単語が多くあります。

https://qiita.com/rema424/items/ee650a1cf99de178cb90

https://jablogs.com/detail/29359

基本的にaxios自体をjestのテスト用のダミーモジュールに入れ替えておくという事をやっているようです。返り値も、そのモックの中で、テストに合う様に返り値を指定しておく方向です。
残念ながら今回はaxiosを使ってる訳ではないのでそのまま参考には出来ません。とはいえ、処理モジュールをモックと入れ替えるという基本方針にはなりそうです。

## jestのモック関数

前述axiosの例では、jestのモック関数を使ってaxiosのモックモジュールを作成している様です。
そのjestのモック関数を見てみます。

https://jestjs.io/ja/docs/mock-function-api

単一の値を返し続ける簡単なものから、呼ばれた順序によって違う結果を返したり、引数によって違う動作をモックに記述したりという事が出来そうです。また、呼ばれた履歴やその時の引数も監視する事が出来る様です。

## Vueでモック関数を使う処理

モック関数で各種処理が出来る事は解りました。そのモック関数をVueのコンポーネント内の関数などと入れ替える必要があります。

https://dev.classmethod.jp/articles/ut-using-jest-spyon/

こちらの記事では、Vueコンポーネントの関数を直接入れ替えているのではなく、Vueコンポーネントで使用する処理をjavaScriptモジュールにして、そのjavaScriptモジュールを`spyOn`という関数の、`mockReturnValue`関数でモック動作を指定しています。

# テストケース実装

今回の自分のケースに適用してみます。

## 対象のVueコンポーネント

```vue:WasmScreen.vue
<template>
  <div :id="id"></div>
</template>
<script lang="ts">
import { defineComponent, toRefs, onMounted, ref } from 'vue';
import init, { start, on_animation } from '@/wasm/package.js'; // wasmモジュールとのインターフェースjs

export default defineComponent({
  name: "WasmScreen",
  setup(props){
    const { id } = toRefs(props)
    const interfaceSetConfig = ref<any>(() => {});
    const interfaceRotate = ref<any>(() => {});
    const setConfig = (type: string, val: number) => {
      const unitedstr = `${type} ${val}`;
      interfaceSetConfig.value(unitedstr);
    };
    const rotate = (axis: string, layer: string, dir: string) => {
      const unitedstr = `${axis} ${layer} ${dir}`;
      interfaceRotate.value(unitedstr);
    };
    const onWasmAnimation = () => {
      // wasmモジュールの関数を呼び出す箇所
      return on_animation();
    };
    const onMountedOperation = () => {
      // エラー発生個所
      init('/wasm/package_bg.wasm').then(() => {
        const [set_config, rotate] = start(id.value);
        interfaceSetConfig.value = set_config;
        interfaceRotate.value = rotate;
      });
    }
    onMounted(onMountedOperation);
    return {
      setConfig,
      rotate,
      onWasmAnimation
    }
  },
  props: {
    id: {type: String, required: true}
  },
})
</script>
<style>
/* 省略 */
</style>
```

wasmモジュールとのインターフェースとなっている`package.js`というモジュールがあります。wasmモジュールをビルドする時に出来る生成物です。Vueのモジュールから直接呼ばれている`init`、`start`、`on_animation`関数をモック化すればよさそうです。

## モジュールを読み込み、モック動作を設定

`package.js`はクラスモジュールでなく、メソッド群になっています。spyOnは監視する関数を持つオブジェクトと、関数名を指定する形です。
今回のケースではspyOnは使えなさそうです（うまい方法あったら教えて下さると嬉しいです）。
これまで紹介してきたページの要素要素を参考にして組み立てた結果として以下のテストケースになりました。各所の説明をコメントとして記載します。

```typescript:WasmScreenTest.spec.ts
import { shallowMount } from '@vue/test-utils'
import WasmScreen from '@/components/WasmScreen.vue'

// jest.mockでモジュール名を指定してmock化します。
jest.mock('@/wasm/package.js');
// モック動作を指定する為、対象モジュールを読み込んでおきます。
import init, { start, on_animation } from '@/wasm/package.js';

// init関数はPromiseを返す関数なので、mockResolvedValueを使います。そのままだとエラーになるので、`as any`でキャストします
(init as any).mockResolvedValue();
// start関数は２つの関数が配列になっているものを返す関数なので、今はダミーの関数を返す様にしておきます。
(start as any).mockImplementation((id: string) => [jest.fn(), jest.fn()]);
// on_animation関数は数値を返す関数なので、mockImplementationを使います
(on_animation as any).mockImplementation(() => 0);

describe('WasmScreen.vue', () => {
  it('success to connect with wasm module', async () => {
    const wrapper = shallowMount(WasmScreen, {
      props: {
        id: 'unittest-id'
      }
    });

    expect(wrapper).toBeDefined();
    // モジュールの関数であるonWasmAnimationをvm経由で呼びだします。
    // モックで指定した値が返ってくる事をチェックします。
    expect(wrapper.vm.onWasmAnimation()).toBe(0);
  })
})
```


# 今回感じた事

Unitテストで行う事をちゃんと決めておく事が重要と思いました。
Unitテストでは、各種条件をテストケースで指定する事が容易です。今回の記事では多くのテストは書いていませんが、細かい条件でそれぞれの部品をテストするのに向いていると思います。

APIの動作なども含めた総合的なテストはE2Eテストを使用する事になると思います。E2Eテストは性質上、細かい多くの動作条件を満たすデータを準備するのが大変です。E2Eテストは少数の、主要な条件の環境を作成し、それぞれの条件下でモジュールの連携が出来るかのテストに使うのが良いと思います。

Unitテスト、Integrationテスト、E2Eテストなどで検索すると使い分けに関する記事を色々見つける事が出来ます。
プロジェクトの性質と保証したい動作の内容によって適材適所で使い分けるという形になると思います。

この事をもっと早く気づいていれば、対応内容ももっと早く導き出せた気がします。

# 今後

テストケースを書く土台は出来たと思うので、細かいテストケース書いていきます。