【Nuxt3】5分で完了！Vitest と Testing Libraryを用いた単体テスト実行環境を作る！

閲覧頂き、ありがとうございます🙇

この記事は `Vitest`と`@nuxt/test-utils`と`Testing Library`を用いた、
単体テスト環境を作成していきます！

# 前提

| name | version |
| - | - |
| Node.js | v20.11.X |
| Nuxt.js(Vue.js) | v3.x |

# 経緯

* `Nuxt3`の環境で単体テストを実行するための環境準備を初めて行なった！
* `Testing Library`を使ってみたい！
* 時間がないので、なるべく`早く`環境を整えたい！
* 日本語の記事があまりない・・・

# 結論

## 以下パッケージの導入で解決🎉

* **Vitest** ⚡️
    * Vite(ビルドツール)ベースのユニットテストフレームワーク
    * ≒ Jest
* **@vitest/coverage-v8** ⚡️
    * Vitestでカバレッジ率を確認するためのライブラリ
    * 実装したコードをどれだけテストできたかの割合とかを出すやつ
* **@nuxt/test-utils** 🫘
    * Nuxt向けのテストユーティリティパッケージ
    * Nuxt3でテストする時に必要なものを色々含めてる便利な福袋的な
* **@testing-library/vue** 🐙
    * Vue用のTesting Libraryのライブラリ
    * ユーザーが実際の操作に近い形でテストを書くことが可能
* **happy-dom** 🎊
    * Node.js環境で動作するDOMのエミュレーター
    * これがないと「ボタンをクリック」みたいなテストでボタンを見つけられない

## 推しポイント🫶

* `公式` で紹介されている！
* Reactのテストで有名な `Testing Library` を `Vue` でも使える！
* `Testing Library` でユーザー目線のテストを行える！
* `Vitest` ですが `Jest` とほぼ同じでテストコードが書ける！
* 基本設定が`5分`で完了する！

# 環境構築

## インストール📝

```Terminal
$ npm i -D vitest @vitest/coverage-v8 @nuxt/test-utils @testing-library/vue happy-dom
```

* すでにNuxt3はインストールされている前提です！

## nuxt.config.ts の修正📝

```nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxt/test-utils/module', // 追加
  ],
})
```

## vitest.config.ts の作成📝

```vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'
import { coverageConfigDefaults } from 'vitest/config'

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
    coverage: {
      exclude: [
        ...coverageConfigDefaults.exclude,
        'nuxt.config.ts',
      ],
    },
  },
})
```

* `environment: 'nuxt'` で Nuxtのランタイム環境での実行となる
    * 要するに**Nuxt環境上でテストコードを実行するよー**ってこと！
* `coverage.exclude` で一部ファイルをテストから除外する
    * `coverageConfigDefaults.exclude` はデフォルトのルール
    * これを書かないとデフォルトが無視されてしまうのでルールを追加するときは**必要**！


## 【任意】 npm scripts の追加📝

```package.json
{
  ~~~~~
  "scripts": {
    "test": "vitest",
    "test:coverage": "vitest run --coverage"
  }
}
```

* `npx`で実行でも大丈夫なので任意にしてます！
* **これで環境構築は完了です**🙌

---

## 実際にテストコードを書いてみる🧑‍💻

### コンポーネントを用意する

```components/AppButton.vue
<script setup lang="ts">
const props = defineProps<{
  buttonText: string
  alertMessage: string
}>()
const showAlert = (message: string) => {
  alert(message)
}
</script>
<template>
  <button @click="showAlert(props.alertMessage)">{{ props.buttonText }}</button>
</template>
```

* 今回は簡単なボタンコンポーネントを用意
* クリックしたらPropsで渡されたテキストをアラート表示する機能を持つ

### コンポーネントのテストコードを書く

```tests/components/AppButton.spec.ts
import AppButton from '~/components/AppButton.vue'
import { renderSuspended } from '@nuxt/test-utils/runtime'
import { screen, fireEvent } from '@testing-library/vue'
import { describe, test, expect, vi } from 'vitest'

// alertのモック
const spyOnAlert = vi.spyOn(window, 'alert').mockImplementation(() => vi.fn())

describe('AppButton', async () => {
  // コンポーネントをレンダリング(Propsもここで渡す)
  await renderSuspended(AppButton, {
    props: {
      buttonText: 'テキスト',
      alertMessage: 'メッセージ',
    },
  })
  // 画面から指定テキストを元にボタンを取得する
  const appButton = screen.getByText('テキスト')

  test('Propsを渡すと正常に表示される', async () => {
    expect(appButton.innerText).toBe('テキスト')
  })
  test('ボタンを押下した時にアラートが表示される', async () => {
    // ボタンをクリックする動作
    await fireEvent.click(appButton)
    expect(spyOnAlert).toHaveBeenCalledOnce()
    expect(spyOnAlert).toHaveBeenCalledWith('メッセージ')
  })
})
```

* テスト内容や各種メソッドはコードのコメントに書いてある通りです！
* `推しポイント`に書いた`ユーザー目線のテスト`と言うのは、
  そのボタンをユーザーが見つけるときは`ボタンに書いてある文字から見つける`👀
  ボタンに振られている`IDで見つけるわけではない`🙅
  といったように、
  テストしたい要素を取得する部分もユーザーの気持ちで行うことができる🫶
  (伝わってるかな・・・w)

## テストの実行✅

### 通常のテスト実行

```
$ npm run test

 ✓ tests/components/AppButton.spec.ts (2)
   ✓ AppButton (2)
     ✓ Propsを渡すと正常に表示される
     ✓ ボタンを押下した時にアラートが表示される

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  23:11:30
   Duration  598ms (transform 220ms, setup 117ms, collect 229ms, tests 2ms, environment 65ms, prepare 113ms)


 PASS  Waiting for file changes...
```

* 全てのテストが通っていることが確認🎉

### カバレッジ付きのテスト実行

```
$ npm run test:coverage

 ✓ tests/components/AppButton.spec.ts (2)
   ✓ AppButton (2)
     ✓ Propsを渡すと正常に表示される
     ✓ ボタンを押下した時にアラートが表示される

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  23:22:15
   Duration  600ms (transform 228ms, setup 119ms, collect 208ms, tests 3ms, environment 61ms, prepare 104ms)

 % Coverage report from v8
-----------------------------------|---------|----------|---------|---------|-------------------
File                               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------------------------|---------|----------|---------|---------|-------------------
All files                          |     100 |      100 |     100 |     100 |                   
 nuxt3-simplest-starter            |     100 |      100 |     100 |     100 |                   
  app.vue                          |     100 |      100 |     100 |     100 |                   
 nuxt3-simplest-starter/components |     100 |      100 |     100 |     100 |                   
  AppButton.vue                    |     100 |      100 |     100 |     100 |                   
-----------------------------------|---------|----------|---------|---------|-------------------
```

* カバレッジについても今回の機能は小規模のため`100%`になっています🎉
  ((100%はあるべき姿だけど、追い求めすぎるのも良くないと聞いたことがある・・・

# 最後に🍵

テストコードは僕もまだまだ勉強中です！

環境構築にいつも悩むので誰かの参考になれば幸いです！
`いいね` や `ストック` 待ってます🙏

`Nuxt3 × @nuxt/eslint` の記事もよかったら是非😉
https://qiita.com/yuki_s_14/items/96620ccfb1dd897ce9f3

# 参考記事📎
* Nuxt.js v3： Testing
    * https://nuxt.com/docs/getting-started/testing
* Vue Testing Library： Intro
    * https://testing-library.com/docs/vue-testing-library/intro
* Vitest： Coverage
    * https://vitest.dev/guide/coverage.html#coverage
* 今回の設定をした実際のデモプロジェクト
    * https://github.com/yuki-s-14/nuxt3-simplest-starter