SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

Vue 3 + TypeScript + Jestの構成で単体テストを実行するために試行錯誤した話

スマートキャンプの20卒エンジニアの高砂です!

私は弊社のSaaS比較サイト「BOXIL」の開発に携わっており、フロントエンドを中心に様々な機能を実装しています。

そんな中、Vue.js + TypeScriptで実装した機能群が複雑になってきた事から「より丁寧にテストを書いていきたい」という気運がチーム内で高まっていました。

そこで、元々Vue.jsが好きな私(下記参照)が試しにJestを触ってみる事にしました。

tech.smartcamp.co.jp

「せっかくならVue.jsはBOXILで使われている2系ではなく最新の3系で試してみたい」という個人的な興味もあり、「Vue 3 + TypeScript + Jest」という組み合わせで新規アプリ開発およびテスト作成に取り組んでみました。

本記事では、それらを実際に進めた際の試行錯誤を紹介していきます!

Vue 3 + TypeScriptでプロジェクト新規作成

まずは新規Vue 3プロジェクトを作っていきます。

今回は最新のVue CLIを採用する事で、素早くVue 3でのSPA構成を構築すると共に、容易なTypeScriptの導入も実現します。

$ yarn global add @vue/cli
yarn global v1.22.10

(中略)

success Installed "@vue/cli@4.5.8" with binaries:
      - vue
✨  Done in 31.51s.
$ vue create hello-vue3
Vue CLI v4.5.8
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

(中略)

🎉  Successfully created project hello-vue3.
👉  Get started with the following commands:

 $ cd hello-vue3
 $ yarn serve

注意点として、TypeScriptを導入する為に下記2点を忘れず行う必要があります(下記画像も併せて参照のこと)。

  1. Manually select featuresオプションを選択する
  2. TypeScriptを選択する

TypeScriptの導入設定

プロジェクト新規作成自体はこれで完了です!

初期ファイルにTypeScriptが使われている事が確認できました。

// src/App.vue

<script lang="ts">
import { defineComponent } from "vue";
import HelloWorld from "./components/HelloWorld.vue";

export default defineComponent({
  name: "App",
  components: {
    HelloWorld
  }
});
</script>

実際に起動してみても、TypeScriptが使われている事が分かるかと思います。

Vue 3 + TypeScriptの初期画面

Jestの導入

次にJestを導入するのですが、ここでいくつかハマりどころがありかなり苦戦したので丁寧に紹介していきます。

失敗1. 公式ドキュメントの手順通りに導入

最初は下記公式ドキュメントを参考に導入してみました。

vue-test-utils.vuejs.org

まずは下記を実行してJestおよびVue Test Utils(単体テストライブラリ)をインストールし、package.jsonに追記を行います。

$ yarn add --dev jest @vue/test-utils
// package.json

{
  "scripts": {
    "test:unit": "jest"
  }
}

続いて下記を実行して*.vueファイルを処理するためのプリプロセッサをインストールし、再度package.jsonに追記を行います。

$ yarn add --dev vue-jest
// package.json

{
  "jest": {
    "moduleFileExtensions": [
      "js",
      "ts",
      "json",
      "vue"
    ],
    "transform": {
      ".*\\.(vue)$": "vue-jest"
    },
    "testURL": "http://localhost/"
  }
}

次に下記を実行し、package.jsonおよびtsconfig.jsonに追記をしてトランスパイルや型推論ができるように変更します。

$ yarn add --dev ts-jest @types/jest
// package.json

{
  "jest": {
    "transform": {
      "^.+\\.tsx?$": "ts-jest"
    }
  }
}
// tsconfig.json

{
  "compilerOptions": {
    "types": [
      "jest"
    ]
  }
}

そして最後に package.jsonに下記を追記して、.tsのテストファイルを実行するように変更を行います。

// package.json

{
  "jest": {
    "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$"
  }
}

これで上手くいく…と思ったのですが、後述のテストを実行してみると下記のエラーが出ました。

Cannot find module 'vue-template-compiler' from 'node_modules/@vue/test-utils/dist/vue-test-utils.js'

そこで「無いのか…とりあえず追加しておこう」とyarn add --dev vue-template-compilerを実行したところ、今度は下記のエラーが出ました。

Vue packages version mismatch:

- vue@3.0.2 (/Users/joe/study/hello-vue3/node_modules/vue/index.js)
- vue-template-compiler@2.6.12 (/Users/joe/study/hello-vue3/node_modules/vue-template-compiler/package.json)

この辺りで「もしかしてVue3系とここまでにインストールしてきたパッケージのバージョンの互換性が無いのでは…?」と思いました。

そこでその辺りについて調べてみると、下記のリポジトリを見つけました。

github.com

ここで「これはVue3系だと公式ドキュメントの通りではダメそうだ」と思い、次のやり方で再導入してみました。

失敗2. 公式ドキュメントの手順 + 一部ライブラリをVue3系向けのバージョンで導入

基本的には失敗1の手順と同じですが、下記ライブラリについては@nextの方をインストールしています。

$ yarn add  --dev @vue/test-utils@next vue-jest@next

この状態なら実行できるかも、と期待したのですが今度は下記のエラーが出ました…。

TypeError: tr.configsFor is not a function

このtr.configsForというのがかなり内部的なモノのようで、類似の事例もほぼ見つからない為、一旦考え方を切り替える事にしました。

失敗3. 公式ドキュメントの手順 + デモリポジトリのバージョンを参考にライブラリを導入

そこで今度は「今回の組み合わせはかなり限定的なバージョンの組み合わせでしか実現できない or そもそも実現不可能なのでは」と捉え、既に動いている実例を探しました。

すると下記リポジトリを見つけたので、これのpackage.jsonおよびyarn.lockを参考にしたバージョンでインストールをしてみました。

github.com

これと同じバージョンなら行ける!と思いきや、またもやエラーが…。

そもそもこのリポジトリはconfigファイルも細かく分けており、一つずつ設定を揃えるのも難しいという判断になりました。

成功? デモリポジトリをcloneしてテスト環境構築

これらの失敗から「現状はVue.js、TypeScript、Jestそれぞれに精通していないと実現は難しい」と思い、先ほど紹介したデモリポジトリをそのまま今回のテスト環境として使わせて頂くことにしました。

本当は自力で環境構築をしたかったのですが、今回のゴールである「『Vue 3 + TypeScript + Jest』という組み合わせでテストを書く」ことを優先し、このような判断としました。

テストを書いてみる

それでは早速、この環境でテストを書いてみます!

今回はテスト用に下記のような画面を作成したので、これに対してテストを記載していこうと思います。

テスト用に編集した初期画面

// src/App.vue

<template>
  <div>
    <h2>Vue 3 + TypeScript + Jest</h2>
    <Hello @say="say" />
    Message: {{ msg }}
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import Hello from './Hello.vue'

export default defineComponent({
  name: 'App',
  components: {
    Hello
  },
  data() {
    return {
      msg: ''
    }
  },
  methods: {
    say() {
      this.msg = 'わーい'
    }
  }
})
</script>
// src/HelloWorld.vue

<template>
  <div class="hello">
    <h3>Contain Test</h3>
    <ul>
      <li>{{ string }}</li>
      <li>{{ integer }}</li>
      <li>{{ float }}</li>
    </ul>
    <h3>Click Test</h3>
    <button @click="say">say</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: "HelloWorld",
  props: {
    msg: String
  },
  data() {
    return {
      string: "smartcamp",
      integer: 100,
      float: 0.1
    };
  },
  methods: {
    say() {
      this.$emit('say')
    }
  }
});
</script>

テストを書く場合、今回だと下記のように記載します。

// src/App.spec.ts

import { mount, shallowMount } from '@vue/test-utils'
import App from './App.vue'
import Hello from './Hello.vue'

test('uses mounts', async () => {
  const wrapper = mount(App)
  expect(wrapper.html()).toContain('Vue 3 + TypeScript + Jest')
  expect(wrapper.html()).toContain('smartcamp')
  expect(wrapper.html()).toContain(100)
  expect(wrapper.html()).toContain(0.1)
  expect(wrapper.html()).toContain('Message: ')

  await wrapper.find('button').trigger('click')
  expect(wrapper.html()).toContain('Message: わーい')
})

test('uses shallowMount', async () => {
  const wrapper = shallowMount(App)
  expect(wrapper.html()).toContain('Vue 3 + TypeScript + Jest')
  expect(wrapper.html()).not.toContain('smartcamp')
  expect(wrapper.html()).not.toContain(100)
  expect(wrapper.html()).not.toContain(0.1)
  expect(wrapper.html()).toContain('Message: ')

  // @ts-ignore
  await wrapper.findComponent(Hello).vm.$emit('say')
  expect(wrapper.html()).toContain('Message: わーい')
})

そして下記を実行する事で、テスト実行とその結果が表示されます!

$ yarn test
yarn run v1.22.10
 PASS  src/App.spec.ts
  ✓ uses mounts (31ms)
  ✓ uses shallowMount (4ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.568s, estimated 3s
Ran all test suites.
✨  Done in 3.59s.

まとめ

初めてこの組み合わせでの実装を試してみましたが、導入自体はほとんどが自動化されているものの、バージョンの調整はかなり難易度が高かったです。

github.com

今回用いたコードは上記リポジトリにまとめてありますので、ぜひ参考にしてみてください。

お読みいただき、ありがとうございました!


[参考]

TypeScript と一緒に使う | Vue Test Utils

Vue.js + Jest + TypeScriptのプロジェクトを作る - Qiita

GitHub - vuejs/vue-test-utils-next: The next iteration of Vue Test Utils, targeting Vue 3

GitHub - lmiller1990/vtu-next-demo: Demo repo with vue-test-utils-next, TS and vue-jest