Vue, Vitest, Testing Library, MSWを使ってテスト駆動開発するチュートリアル

今回は以下のライブラリを中心にVueにおけるテスト駆動開発(TDD)の進め方を説明します。

- Vue3
- Vitest
- Testing Library
- Mock Service Worker

Options APIで書きますが、テストコードはComposition APIでも動くので、
Composition APIの実装に多少慣れてる人はぜひとも挑戦してください。

今回の記事の中で作ったコードは以下のリポジトリに収めました。

https://github.com/TakahiRoyte/vue-tdd-sample

## テスト駆動開発(TDD)ってなに？

TDDとはTest Driven Development（テスト駆動開発）の略であり、その文字通り、
テストを先に書いてその後にそのテストを満たすコードを書くことを繰り返しながら進める開発手法です。
XP(Extreme Programming)のアプローチの1つとしてとても有名です。
実施においては以下の図のRedから順番に開発をサイクルで回していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/8c5c066b-a725-35f7-f0bc-d31e003a4f2a.png)

細かい仕様ごとにRedに立ち返るようなサイクルで開発できると良いです。
例えば、バリデーションエラー表示つきの入力フォーム1つをとっても以下の1つずつテストを回します。

1. 入力フォームが表示されていること
1. 入力フォームに入力があった場合、値が表示されていること
1. 入力フォームに誤った値がある時にエラーが表示されること
1. 入力フォームに正しい値がある時にエラーが表示されないこと

まれにある勘違いとして、「テスト工程が開発工程の前に来る」という誤解がありますがそうではなく、
テスト「駆動」開発なので「テストを書く⇒開発」を小さい単位で進めていくことが重要です。
工程は明確に分かれるのではなくほぼ同時に進めるイメージです。

## 環境構築

必要なライブラリをインストールします。
Vueのベースとなるプロジェクトをどう作ったかによってインストール済みのライブラリが変わります。
よくある例としては`npm init vue@latest`か`npm create vite@latest`の2通りです。
`vue@latest`を使う場合はオプションでUnit Testingの環境も込みで作成可能です。
どちらにせよ、自分の環境に合わせて必要なものは追加してください。

```bash
$ npm install -D vitest @vue/test-utils jsdom @testing-library/vue
```

これで必要なライブラリがインストールできたら、次はVitestを起動するスクリプトを書きます。
`package.json`の`scripts`に`"test:unit"`を追加しましょう。

```js:package.json
{
  ...
  "scripts":{
    "dev": "vite",
    "build": "vite build",
    "test:unit": "vitest --environment jsdom",
    ...
  },
  ...
}
```

この状態で追加したコマンドを実行します。

```bash
$ npm run test:unit

> vue-vite-ts@0.0.0 test:unit
> vitest --environment jsdom


 DEV  v0.18.0 C:/dev/occfront-des

include: **/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}
exclude:  **/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**        
watch exclude:  **/node_modules/**, **/dist/**

No test files found, exiting with code 1
```

これでテストが走りました。テストコードがない場合は上の表示になります。
`vue@latest`ベースの場合は1つサンプルがあるのでテストがパスするはずです。
ただ、今回は新たなコンポーネントを作成してそれをベースにテストを進めようと思うので削除してもOKです。

## TDDのチュートリアル

まずは作るページの仕様が決まっていないとTDDできないので、
サインアップのページを作っていきます。

- Sign Upのヘッダーが存在する
- 登録用フォーム
  - ユーザー名の入力
  - メールアドレスの入力
  - パスワードの入力
  - パスワード再確認の入力
  - サインアップボタン
- 登録ボタンを押したらサーバーにリクエストが送られる
- サーバーからエラーが帰ってきた場合、エラーメッセージを表示する

まずはベースとなるテストのファイルを用意します。
vitestはディレクトリ内の`spec.js`を探して実行してくれるので、
保存場所はどこでも大丈夫ですが私はコンポーネントと同じ場所にセットで保存してます。
理由は`spec.js`の`spec`はSpecification、つまり仕様を意味します。
テストコード＝仕様書となるように更新し続けられるとベストですよね（実際は色々あるけども）。

```js:SignUpPage.spec.js
import SignUpPage from './SignUpPage.vue'
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/vue'
```

```vue:SignUpPage.vue
<template>
</template>
```

このぐらいでOKです。

### レイアウトのTDD

まずは「Sign Upのヘッダーが存在する」のテストコード実装します。

```js:SignUpPage.spec.js
import SignUpPage from './SignUpPage.vue'
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/vue'

it('Sign Upヘッダーが表示される', () => {
  render(SignUpPage)
  const header = screen.getByRole('heading', { name: 'Sign Up' })
  expect(header).toBeTruthy()  
}
```

インポートした`SignUpPage`をレンダリングして、
レンダリング結果の`screen`から`getByRole`で`Sign Up`とう文字列を含んだ要素を取得しています。
この結果が存在するかどうかを`expect(header).toBeTruthy()`というアサーション（検証条件）で、
「ヘッダー(header)がTruthy(true判定される)な値であることを期待(expect)する」とテストしています。

これを実際にテスト実行してみましょう。
失敗してFAILEDと表示されると思います。

```bash
$ npm run test:unit

> vue-test-project@0.0.0 test:unit
> vitest --environment jsdom     

 DEV  v0.18.1 C:/dev/vue-test-with-vitest

 ❯ src/pages/SignUpPage.spec.js (1)
   × Sign Upヘッダーが存在する     

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
 FAIL  src/pages/SignUpPage.spec.js > Sign Upヘッダーが表示される
TestingLibraryElementError: Unable to find an accessible element with the role "heading" and name "Sign Up"

There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the `hidden` option to `true`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole

Ignored nodes: comments, <script />, <style />
<body>
  <div />
</body>
 ❯ Object.getElementError node_modules/@testing-library/dom/dist/config.js:40:19
 ❯ node_modules/@testing-library/dom/dist/query-helpers.js:90:38
 ❯ node_modules/@testing-library/dom/dist/query-helpers.js:62:17
 ❯ node_modules/@testing-library/dom/dist/query-helpers.js:111:19
 ❯ src/pages/SignUpPage.spec.js:9:29
      7|     it('Sign Upヘッダーが存在する', () => {
      8|       render(SignUpPage)
      9|       const header = screen.getByRole('heading', { name: 'Sign Up' })
       |                             ^
     10|       expect(header).toBeTruthy()

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files  1 failed (1)
     Tests  1 failed (1)
      Time  2.49s (in thread 64ms, 3893.38%)


 FAIL  Tests failed. Watching for file changes...
       press h to show help, press q to quit
```

みなさんのターミナルにはもう少し見やすいカラーで表示されていると思いますが、
どこでテストが落ちたのかも一目瞭然でわかるようになってます。
getByRoleでアクセスしようとした要素が無いというエラー(`TestingLibraryElementError`)ですね。
これが冒頭に説明した図でいうRedの状態、テストを先に作成して落ちている状態です。
次に、このテストが通るよう実装をコンポーネント側にしていきます。

```vue:SignUpPage.vue
<template>
  <h1>Sign Up</h1>
</template>
```

するとどうでしょうか。テストが自動的に再実施され以下のように表示されると思います。

```bash
 ✓ src/pages/SignUpPage.spec.js (1)

Test Files  1 passed (1)
     Tests  1 passed (1)
      Time  122ms


 PASS  Waiting for file changes...
       press h to show help, press q to quit
```

`PASS`と表示されましたね。これがGreenの状態です。
このようにテストは高速に再実施されるので、実装に誤りがあった時はすぐ検出できます。
実装で思わぬ副作用からバグを埋め込んでも積上げたテストコードが教えてくれる可能性が増えるわけです。
テストコードが実装とともに増えていくのでアプリ＆テストの両面でリファクタリングがしやすくなります。

TDDのRefactoringを実施しましょう。
ヘッダーはリファクタの余地ないので、テストコード側をリファクタします。
`it`でテストを表現してましたが`describe`も組合せてみましょう。
`it`は一つのテストを意味し、`describe`は複数のテストをまとめるための関数です。

```js:SignUpPage.spec.js
...

describe('SignUpPage', () => {
  it('Sign Upヘッダーが表示される', () => {
    render(SignUpPage)
    const header = screen.getByRole('heading', { name: 'Sign Up' })
    expect(header).toBeTruthy()
  })
})
```

この`describe`と`it`は変わった関数名ですが、実は英語だとわかりやすいんです。
`Describe SignUpPage, it has Sign Up as a header`、
（SignUpPageの説明、Sign Upヘッダーが存在する）の用に通常の文章の用に仕様が書けるのです。
ただ日本語なので若干そこのニュアンスは伝えづらいですが、そういうものだと理解しておきましょう。
この状態でもテストが通ることを確認できたら順調です。
続いて、ユーザー名の入力フォームのテストを実装します。

```js:SignUpPage.spec.js
...
import { describe, it, expect, afterEach } from 'vitest'
import { render, screen, cleanup } from '@testing-library/vue'

describe('SignUpPage', () => {
  afterEach(cleanup)
  it('Sign Upヘッダーが表示される', () => {
    render(SignUpPage)
    const header = screen.getByRole('heading', { name: 'Sign Up' })
    expect(header).toBeTruthy()
  })

  it('ユーザー名の入力フォームが表示される', () => {
    const { container } = render(SignUpPage)
    const input = container.querySelector('input')
    expect(input).toBeTruthy()
  })
})
```

今度は`render`結果の`container`([Document](https://testing-library.com/docs/vue-testing-library/api#container))から、`input`要素を探し存在チェックをするテストです。
複数テストになったタイミングで`afterEach(cleanup)`が追加されました。
これは`render`後にレンダー結果をリセットするためにいれます。
cleanupがないとレンダ結果が積み上がりテストが壊れます（Jestは自動でcleanupが走るがVitestは走らない）。
このテストで保存するとFAILになるので、PASSするようコンポーネントを実装しましょう。

```vue:SignUpPage.vue
<template>
  <h1>Sign Up</h1>
  <input type="text">
</template>
```

これで良いのか？と思うかもしれませんが一旦このまま進みます。
続いて、Eメールアドレス用入力フォームのテストを書きます。
```js:SignUpPage.spec.js
...

describe('SignUpPage', () => {
  ...
  it('ユーザー名の入力フォームが表示される', () => {
    const { container } = render(SignUpPage)
    const input = container.querySelector('input')
    expect(input).toBeTruthy()
  })

  it('Eメールの入力フォームが表示される', () => {
    const { container } = render(SignUpPage)
    const input = container.querySelector('input')
    expect(input).toBeTruthy()
  })
})
```

さて、テストが落ち……ずに通ってしまいました。
これは当たり前で、両テストとも`input`の要素が存在していることしかチェックしていないからです。
より良いテストをするためにテストを書き直しましょう。
入力フォームは通常ラベル`label`と併用されることが多いので、それを利用したテストにします。

```js:SignUpPage.spec.js
describe('SignUpPage', () => {
  ...
  it('ユーザー名の入力フォームが表示される', () => {
    render(SignUpPage)
    const input = screen.queryByLabelText('ユーザー名')
    expect(input).toBeTruthy()
  })

  it('メールアドレスの入力フォームが表示される', () => {
    render(SignUpPage)
    const input = screen.queryByLabelText('メールアドレス')
    expect(input).toBeTruthy()
  })
})
```

これでテストが落ちるので、コンポーネントを更新しましょう。

```vue:SignUpPage.vue
<template>
  <h1>Sign Up</h1>
  <label for="username">ユーザー名</label>
  <input id="username" type="text" />
  <label for="email">メールアドレス</label>
  <input id="email" type="text" />
</template>
```

これでテストが通るはずです。
これでちゃんとテストの意味があるのかを確認するためにも、
`label`の文字を変えてみたり、`input`要素を削除してテストが失敗するかを確認してみてください。
同じ要領でパスワードとパスワード確認のテストと実装も進めてみましょう。
テストは問題ないとは思いますが、以下のようにパスワードが表示されてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89911/afd6a35d-371e-c3d8-f8b5-d73b666e1447.png)

これを避けるためには`input`を`type="password"`にしないといけませんが、
それもテストしてみましょう。

```js:SignUpPage.spec.js
...
  it('パスワードの入力フォームのtypeがpasswordであること', () => {
    render(SignUpPage)
    const input = screen.getByLabelText('パスワード')
    expect(input.type).toBe('password')
  })
...
```

`input`要素の`type`属性が`password`であることを確認しています。
こんな形で要素の属性にアクセスできるので他のケースでもよく使うやり方です。
まとめてテストを進めるのはあまり推奨されませんがここまで同じ作業なら多少はokです。
パスワードとパスワード確認の両テストを実装しRedになったらコンポーネントを実装してください。

```vue:SignUpPage.vue
<template>
  <h1>Sign Up</h1>
  <label for="username">ユーザー名</label>
  <input id="username" type="text" />
  <label for="email">メールアドレス</label>
  <input id="email" type="text" />
  <label for="password">パスワード</label>
  <input id="password" type="password" />
  <label for="passwordcheck">パスワード確認</label>
  <input id="passwordcheck" type="password" />
</template>
```

`input`周りのテストが終わったら次は登録ボタンです。
ボタンはまた`getByRole`で要素の取得をします。

```js:SignUpPage.spec.js
...
  it('登録用ボタンが表示される', () => {
    render(SignUpPage)
    const button = screen.getByRole('button', { name: '登録' })
    expect(button).toBeTruthy()
  })
})
```

テストがRedになったらGreenにするための実装をしましょう。

```vue:SignUpPage.vue
<template>
  <h1>Sign Up</h1>
  ...
  <button>登録</button>
</template>
```

初期表示時には登録ボタンが押せないようにいたいため、`disabled`のテストも追加します。

```js:SignUpPage.spec.js
...
  it('登録ボタンが初期表示時はdisabledとなっている', () => {
    render(SignUpPage)
    const button = screen.getByRole('button', { name: '登録' })
    expect(button.disabled).toBeTruthy()
  })
})
```

Red ⇒ Green にしましょう。

```vue:SignUpPage.vue
<template>
  <h1>Sign Up</h1>
  ...
  <button disabled>登録</button>
</template>
```

ここまでできたら初期表示のレイアウトとしてテストは十分なので、
一旦次に備えてこれまでのテストを`describe`で囲み、レイアウトのテストであることを明記します。

```js:SignUpPage.spec.js
...
describe('SignUpPage', () => {
  afterEach(cleanup)

  describe('レイアウト', () => {
    // これまでのテスト
  })
})
```

### インタラクションのTDD

レイアウトのTDDは静的な状態に対するテストが主だったのでシンプルでした。
インタラクションのTDDはコンポーネントの`<script>`も実装するので少しだけ難易度があがります。
まずは先程設定した`disabled`が、
「全フォームの内容が入力済かつパスワードが一致」の場合に解除されるテストを書いてみましょう。

```js:SignUpPage.spec.js
...
import { render, screen, cleanup, fireEvent } from '@testing-library/vue'
...
describe('SignUpPage', () => {
  ...
  describe('インタラクション', () => {
    it('全フォーム入力済み、かつパスワードとパスワード確認が同じ値の場合、登録のdisabledが解除される', async () => {
      render(SignUpPage)
      const usernameInput = screen.getByLabelText('ユーザー名')
      const emailInput = screen.getByLabelText('メールアドレス')
      const passwordInput = screen.getByLabelText('パスワード')
      const passwordCheckInput = screen.getByLabelText('パスワード確認')
      await fireEvent.update(usernameInput, 'Usern')
      await fireEvent.update(emailInput, 'user@example.com')
      await fireEvent.update(passwordInput, 'P4ssw0rd')
      await fireEvent.update(passwordCheckInput, 'P4ssw0rd')
      const button = screen.getByRole('button', { name: '登録' })
      expect(button.disabled).toBe(false)
    })
  })
})
```

レイアウトと分けてグループ化するためにインタラクションの`describe`を作ります。
その中で、レイアウトでも使った`input`要素の取得をしながら、
`fireEvent`を新たにインポートして使っていることがわかります。
これはTesting LibraryによるAPIで様々なイベントを発火させることができます。
今回は`update`イベントを使って指定した`input`要素に値を入力しています。
`fireEvent`はVueの画面更新の特性上非同期なので、`async/await`を追加しています。
保存してRedになったらGreenにするために実装しましょう。

```vue:SignUpPage.vue
<template>
  <h1>Sign Up</h1>
  <label for="username">ユーザー名</label>
  <input id="username" type="text" v-model="username" />
  <label for="email">メールアドレス</label>
  <input id="email" type="text" v-model="email" />
  <label for="password">パスワード</label>
  <input id="password" type="password" v-model="password" />
  <label for="passwordcheck">パスワード確認</label>
  <input id="passwordcheck" type="password" v-model="passwordCheck" />
  <button :disabled="isDisabled">登録</button>
</template>

<script>
export default {
  name: 'SignUpPage',
  data() {
    return {
      username: '',
      email: '',
      password: '',
      passwordCheck: '',
    }
  },
  computed: {
    isDisabled() {
      if (
        this.username &&
        this.email &&
        this.password &&
        this.password === this.passwordCheck
      )
        return false
      return true
    },
  },
}
</script>
```

一気に実装しましたが、そんなに複雑なことはしていません。
各フォームに対して`v-model`を使って値を`data`と紐づけます。
その次にボタンの活性非活性を管理する`disabled`を`computed`に作り、
指定の条件下で`true/false`を返すようにしています。
試しに、パスワードが不一致の場合にエラーになるかをチェックするテストを追加しましょう。

```js:SignUpPage.js
...
    it('全フォーム入力済でも、パスワードが不一致の場合、登録ボタンがdiasbledになる', async () => {
      render(SignUpPage)
      const usernameInput = screen.getByLabelText('ユーザー名')
      const emailInput = screen.getByLabelText('メールアドレス')
      const passwordInput = screen.getByLabelText('パスワード')
      const passwordCheckInput = screen.getByLabelText('パスワード確認')
      await fireEvent.update(usernameInput, 'Usern')
      await fireEvent.update(emailInput, 'user@example.com')
      await fireEvent.update(passwordInput, 'P4ssw0rd')
      await fireEvent.update(passwordCheckInput, 'password')
      const button = screen.getByRole('button', { name: '登録' })
      expect(button.disabled).toBe(true)
    })
  })
})
```

今回は特に何もしなくてもGreenになると思います。
先程のコードと重複している点が多いので、テストコードをリファクタしましょう。
全項目を入力するメソッドを切り出します。

```js:SignUpPage.js
...
  describe('インタラクション', () => {
    it('全フォーム入力済、かつパスワードとパスワード確認が同じ値の場合、登録のdisabledが解除される', async () => {
      render(SignUpPage)
      await fillAllForm('Usern', 'user@example.com', 'P4ssw0rd', 'P4ssw0rd')
      const button = screen.getByRole('button', { name: '登録' })
      expect(button.disabled).toBe(false)
    })

    it('全フォーム入力済でも、パスワードが不一致の場合、登録ボタンがdiasbledになる', async () => {
      render(SignUpPage)
      await fillAllForm('Usern', 'user@example.com', 'P4ssw0rd', 'password')
      const button = screen.getByRole('button', { name: '登録' })
      expect(button.disabled).toBe(true)
    })
  })
})

async function fillAllForm(username, email, passowrd, passwordCheck) {
  const usernameInput = screen.getByLabelText('ユーザー名')
  const emailInput = screen.getByLabelText('メールアドレス')
  const passwordInput = screen.getByLabelText('パスワード')
  const passwordCheckInput = screen.getByLabelText('パスワード確認')
  await fireEvent.update(usernameInput, username)
  await fireEvent.update(emailInput, email)
  await fireEvent.update(passwordInput, passowrd)
  await fireEvent.update(passwordCheckInput, passwordCheck)
}
```

実装やテストに手を入れてもGreenのままであればリファクタリングがうまくいった証拠です。
テストコードがあることで実装や変更に対して大胆に挑戦することができるようになります。
続いては同じ関数を使えるAPI呼び出しのテストを書いていきます。
今回はHTTPクライアントであるAxiosを利用するので、まずはインストールします。

```bash
$ npm install axios
```

新たなライブラリをインストールした時は一度VitestやViteは立ち上げ直した方が無難です。
続いてはテストコードを書いていきます。

```js:SignUpPage.spec.js
import SignUpPage from './SignUpPage.vue'
import { describe, it, expect, afterEach, vi } from 'vitest'
import { render, screen, cleanup, fireEvent } from '@testing-library/vue'
import axios from 'axios'
...

    it('登録ボタン押下時にユーザー名、メールアドレス、パスワードをサーバーに送信する', async () => {
      render(SignUpPage)
      await fillAllForm('Usern', 'user@exmaple.com', 'P4ssw0rd', 'P4ssw0rd')
      const button = screen.getByRole('button', { name: '登録' })

      const mockFn = vi.fn()
      axios.post = mockFn

      await fireEvent.click(button)

      const firstCall = mockFn.mock.calls[0]
      const body = firstCall[1]

      expect(body).toEqual({
        username: 'Usern',
        email: 'user@example.com',
        password: 'P4ssw0rd',
      })
    })
  })
})
```

`import`では先程インストールした`axios`と`vitest`から`vi`をインポートしてます。
コンポーネント内でAxiosが呼ばれたとしても実際にサーバーにPOSTさせるのではなく、
`vi`でモックを作成しそのモックが正しく呼び出されたかを検証するイメージです。
`vi.fn()`のモック関数を`axios.post`に再代入して上書きしてモック化しています。
その後に`fireEvent.click`でボタンを押し、`mockFn.mock.calls[0]`でモックの呼び出し、
そして実際に呼び出された際のリクエストボディが定義どおりに呼び出されてるかアサーションでテストします。
テストがRedになったので、Greenにしていきます。

```vue:SignUpPage.vue
<template>
  ...
  <button :disabled="isDisabled" @click="submit">登録</button>
</template>

<script>
import axios from 'axios'

export default {
  ...
  methods: {
    submit() {
      axios.post('/api/v1/users', {
        username: this.username,
        email: this.email,
        password: this.password,
      })
    },
  },
}
</script>
```

これでGreenになるはず！と思ったらFAILのまま……
テスト結果を確認してみます。

```bash
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/pages/SignUpPage.spec.js > SignUpPage > インタラクション > 登録ボタン押下時にユーザー名、メールアドレス、パスワードをサーバーに送信する
AssertionError: expected { username: 'Usern', …(2) } to deeply equal { username: 'Usern', …(2) }
 ❯ src/pages/SignUpPage.spec.js:93:20
     91|       const body = firstCall[1]
     92| 
     93|       expect(body).toEqual({
       |                    ^
     94|         username: 'Usern',
     95|         email: 'user@example.com',

  - Expected  - 1
  + Received  + 1
  
    Object {     
  -   "email": "user@example.com",
  +   "email": "user@exmaple.com",
      "password": "P4ssw0rd",
      "username": "Usern",
    }

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files  1 failed (1)
     Tests  1 failed | 11 passed (12)
      Time  302ms


 FAIL  Tests failed. Watching for file changes...
```

アサーションが原因をわかりやすく表してくれてますね。
`user@exmaple.com`とメールアドレスにタイポがありました。
こういうのを気づかせてくれるのもTDDの良いところ。修正するとGreenになりました。

Axiosをモック化するこの方法でもテスト自体は可能ですが、
最近はMock Service Worker(MSW)というライブラリを使ったモッキングがオススメです。
MSWはその名の通りWebの[Service Worker](https://developer.mozilla.org/ja/docs/Web/API/Service_Worker_API)であり、
ネットワークレベルでリクエストをインターセプトしてモックしたレスポンスを返すことができます。
これの何がメリットかというと、アプリケーションレベルではモックがない、
アプリケーションの実装に依存しないモックになるという点です。
例えばAxiosからFetchやjQueryやSuperAgentに乗り換える場合、
Axiosをモック化している場合はテストコードも修正が必要ですが、
MSWでレスポンスをモック化してしまえばそれも不要になるということです。
実際にやっていきましょう。まずインストールします。

```bash
$ npm install -D msw
```

そしてテストを実装していきます。

```js:SignUpPage.spec.js
import SignUpPage from './SignUpPage.vue'
import { describe, it, expect, afterEach } from 'vitest'
import { render, screen, cleanup, fireEvent } from '@testing-library/vue'
import { setupServer } from 'msw/node'
import { rest } from 'msw'

describe('SignUpPage', () => {
  ...
  it('登録ボタン押下時にユーザー名、メールアドレス、パスワードをサーバーに送信する', async () => {
      let requestBody
      const server = setupServer(
        rest.post('/api/v1/users', async (req, res, ctx) => {
          requestBody = await req.json()
          return res(ctx.status(200))
        })
      )
      server.listen()

      render(SignUpPage)
      await fillAllForm('Usern', 'user@example.com', 'P4ssw0rd', 'P4ssw0rd')
      const button = screen.getByRole('button', { name: '登録' })
      await fireEvent.click(button)

      await server.close()

      expect(requestBody).toEqual({
        username: 'Usern',
        email: 'user@example.com',
        password: 'P4ssw0rd',
      })
    })
  })
})

```

import文から`axios`や`vi`を消して`msw`周りから`setupServer`と`rest`を追加しています。
`setupServer`は文字通りサーバーの設定で、`rest`はREST APIベースのモックを提供するものです。
実際のコードでは`requestBody`を変数定義してリクエストボディの受け皿として用意しています。
`setupServer`でサーバー準備し、その中のリクエストハンドラで`rest`を利用します。
これで`/api/v1/users`に対するPOSTリクエストをインターセプトしハンドラーの処理を実行します。
あとはモックサーバーの開始終了で`listen()`と`close()`を実施するだけです。
このリファクタリングを加えた場合でもテストがGreenのままであることを確認してください。
続いて、サーバーからエラーが返ってきたパターンも実装してみましょう。

```js:SignUpPage.spec.js
  ...
    it('登録時にサーバーからエラーが返された場合、エラーメッセージを表示する', async () => {
      const server = setupServer(
        rest.post('/api/v1/users', async (req, res, ctx) => {
          return res(
            ctx.status(500),
            ctx.json({
              error: {
                message: 'サーバーエラーです。時間を置いて試してください。',
              },
            })
          )
        })
      )
      server.listen()

      render(SignUpPage)
      await fillAllForm('Error1', 'user@example.com', 'P4ssw0rd', 'P4ssw0rd')
      const button = screen.getByRole('button', { name: '登録' })
      await fireEvent.click(button)
      await server.close()

      const text = await screen.findByText(
        'サーバーエラーです。時間を置いて試してください。'
      )
      expect(text).toBeTruthy()
    })
  })
})
```

MSWのモックサーバーがステータスコード500でエラーメッセージを返すようにしています。
そのエラーメッセージがそのまま画面に表示されるのを確認するために、
`queryByText`ではなく`findByText`をawaitで使っています。
`findBy~`は後から画面が更新される場合に非同期で使えるので便利です。
RedになったらGreenにしていきましょう。

```vue:SignUpPage.vue
<template>
  <form>
    ...
    <button :disabled="isDisabled" @click.prevent="submit">登録</button>
    <p v-if="errorMessage">{{ errorMessage }}</p>
  </form>
</template>

<script>
...
  data() {
    return {
      ...
      errorMessage: '',
    }
  },
  ..
  methods: {
    async submit() {
      try {
        await axios.post('/api/v1/users', {
          username: this.username,
          email: this.email,
          password: this.password,
        })
      } catch (error) {
        this.errorMessage = error.response.data.error.message
      }
    },
  },
}
</script>
```

axiosはサーバーからのエラーをエラーとして投げるので、
`try/catch`で返ってきたエラーのレスポンスを`data`の`errorMessage`に突っ込みます。
その値を元に`v-if`と`{{ }}`でメッセージがある時のみ表示しています。
これでテストがGreenになるはずです。
テストコード側のリファクタリングを少ししましょう。
MSWのサーバーを作成するコードが少し冗長になっています。
切り出してインタラクションの`describe`のすぐ下に移動させてみます。

```js:SignUpPage.spec.js
  describe('インタラクション', () => {
    // モックサーバー準備
    let requestBody
    const server = setupServer(
      rest.post('/api/v1/users', async (req, res, ctx) => {
        requestBody = await req.json()
        if (requestBody.username === 'Error1') {
          return res(
            ctx.status(500),
            ctx.json({
              error: {
                message: 'サーバーエラーです。時間を置いて試してください。',
              },
            })
          )
        }
        return res(ctx.status(200))
      })
    )

    async function responseServerCheck(username) {
      server.listen()
      render(SignUpPage)
      await fillAllForm(username, 'user@example.com', 'P4ssw0rd', 'P4ssw0rd')
      const button = screen.getByRole('button', { name: '登録' })
      await fireEvent.click(button)
      await server.close()
    }
    ...

    it('登録ボタン押下時にユーザー名、メールアドレス、パスワードをサーバーに送信する', async () => {
      await responseServerCheck('Usern')
      expect(requestBody).toEqual({
        username: 'Usern',
        email: 'user@example.com',
        password: 'P4ssw0rd',
      })
    })

    it('登録時にサーバーからエラーが返された場合、エラーメッセージを表示する', async () => {
      await responseServerCheck('Error1')
      const text = await screen.findByText(
        'サーバーエラーです。時間を置いて試してください。'
      )
      expect(text).toBeTruthy()
    })
  })
})
```

リクエストハンドラーの中で`requestBody`の`username`により分岐を設けています。
そのため`username`を元にレスポンスを取得する`serverResponseCheck`関数を切りだしました。
この方法でサーバーから返すステータスコードやレスポンスデータのパターンを設けることが可能です。
この記事では最後に少しリファクタリングして終わりにしたいと思います。

```js:SignUpPage.spec.js
import SignUpPage from './SignUpPage.vue'
import { describe, it, expect, afterEach, beforeEach } from 'vitest'
import { render, screen, cleanup, fireEvent } from '@testing-library/vue'
import { setupServer } from 'msw/node'
import { rest } from 'msw'

describe('SignUpPage', () => {
  beforeEach(() => {
    render(SignUpPage)
  })
  afterEach(cleanup)

  describe('レイアウト', () => {
    it('Sign Upヘッダーが表示される', () => {
      const header = screen.getByRole('heading', { name: 'Sign Up' })
      expect(header).toBeTruthy()
    })

    it('ユーザー名の入力フォームが表示される', () => {
      const input = screen.getByLabelText('ユーザー名')
      expect(input).toBeTruthy()
    })

    it('メールアドレスの入力フォームが表示される', () => {
      const input = screen.getByLabelText('メールアドレス')
      expect(input).toBeTruthy()
    })
    ...
})
```

どのテストでも必ず行っている`render(SignUpPage)`を、
vitestの`beforeEach`を使って毎テストの最初に行うよう設定しました。
他に実装していた`render(SignUpPage)`はすべて削除しています。
これでもGreenのままであることを確認できたらOKです。
だいぶテストの内容に集中したコードになってるかと思います。

## 伝えたいこと

TDDを始めると言う時、最初はテストの書き方そのものに慣れていないので、
とてもまどろっこしかったり、コストがかかる！なんて思われがちかと思います。
ですが、コードがシンプルなうちにテストコードを書くほうが圧倒的に簡単です。
モブプロでもしていない限り誰が書いたかわからないコードを読み解きながら、
テストコードを書くのは結構しんどい作業です。
コンポーネントの構造が複雑化していたらなおさらです。
基本時間が経つに連れ難易度は右肩上がりになります。
それよりもコードベースが少ないうちからテストを書くのに慣れていって、
徐々にテスト、コード、スキルを積み重ねていくのが圧倒的にオススメです。
ぜひ、次なる挑戦でもTDDに取り組んでみてください。

## TDDで守ってほしい方針

- できるだけ早い段階でTDDを導入する
  - 「伝えたいこと」にも書きましたが大事なことなのでここでも
- 1テスト(`it`)、1アサーション(`expect`)にする
  - エラーがどこで起きたかが一目瞭然になる
  - テストの内容も具体的にできる
- リファクタリングを積極的に行う
  - TDDはGreenに一度できたら品質を保ったままリファクタに挑戦する土台ができているということ
  - アプリもテストもどちらのコードもリファクタリング
  - ボーイスカウトルールみたいにやる

## その他

Vitestはテストが爆速なのであまり困ることは無いとは思いますが、
重たい特定のテストだけをスキップして効率化をしたかったり、
1つの画面開発中には1つの画面に紐づくテストだけを実施したい時があるかもしれません。
そんな時は以下を参考にしてください。`it`だけじゃなくて`describe`にも使えます。

**特定のテストだけ実行したい場合**

```javascript
it.only('テストケース名', () => {})
```

**特定のテストをスキップしたい場合**

```javascript
it.skip('テストケース名', () => {})
```

**複数のテストを同時並行に実行したい場合**

```javascript
it.concurrent('テストケース名', () => {})
```