Vue 3 テストのチュートリアル
Vueでテストを書く
Vueでのテストの書き方を知るために、チュートリアルを作ってみました。
セットアップ
成果物のリポジトリはこちら: https://github.com/naga3/vue-test-example
# yarn global add @vue/cli # Vue CLIが入っていない場合
vue create vue-test-example
cd vue-test-example
yarn serve
以下のオプションで作成しました。Unit Testing
だけは有効にして、あとはご自由にどうぞ。
Please pick a preset: Manually select features
Check the features needed for your project: Babel, TS, Router, CSS Pre-processors, Linter, Unit
Choose a version of Vue.js that you want to start the project with 3.x
Use class-style component syntax? No
Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX
)? Yes
Use history mode for router? (Requires proper server setup for index fallback in production) Yes
Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SC
SS (with dart-sass)
Pick a linter / formatter config: Basic
Pick additional lint features: Lint on save
Pick a unit testing solution: Jest
Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
Save this as a preset for future projects? No
作られたテストの内容を眺めてみる
Unit Testing
を有効にするとサンプルのテストがひとつ作られますので、それを眺めてみます。
テスト対象のコンポーネント
msg
というプロパティを受け取って、それを表示するだけの単純なコンポーネントです。
テストファイル
describe はテストをグルーピングするもので、テスト中に概要 (HelloWorld.vue) が表示されるので、どのテストで落ちたかを知るためにも有用です。describe
は入れ子にすることができます。
it の中に実際のテストを書いて行きます。it
は test
の別名で、どちらで書いても構いません。
コンポーネントをテストするためにはまずマウントする必要があり、そのために mount
と shallowMount
のどちらかを使います。mount
は子コンポーネントも含めた完全な状態でマウントされますが、shallowMount
は子コンポーネントはスタブになります。くわしくは こちら を参照してください。
props
にはコンポーネントに渡すプロパティを記述します。
mount
または shallowMount
はコンポーネントをマウントしたインスタンスの薄いラッパー VueWrapper を返します。VueWrapper
はテストで有用なメソッドがいくつか実装されていて、text はコンポーネントのテキストを返します。
テスト実行
yarn test:unit
$ yarn test:unit
yarn run v1.22.18
$ vue-cli-service test:unit
PASS tests/unit/example.spec.ts
HelloWorld.vue
✓ renders props.msg when passed (17 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.159 s, estimated 2 s
Ran all test suites.
Done in 1.70s.
describe
と it
に書いた説明が表示されているのが確認できます。
簡単なコンポーネントのテストを書いてみる
では実際にテストを書いてみます。最初は簡単なフォームコンポーネントを作成します。
コンポーネント作成
このようなコンポーネントを作成します。
コメントを書いてボタンを押すと、メッセージが表示されます。
コンポーネントのソースはこちら。
-
comment
は入力フィールドに入力した文字列です。 -
receiver
は受信者で、"A", "B", null
のいずれかが入ります。
data-testid
属性はテストの要素を特定するために設定しています。IDやclassから特定することもできるのですが、そうするとデザインの変更にテストが引っ張られてしまいますので、data-*
属性を使ったほうがベターです。詳しくはこちらの記事を参照してください。 https://qiita.com/akameco/items/519f7e4d5442b2a9d2da
テストを実装する
このフォームのテストではどのようなテスト項目が必要かを考えてみます。
- 最初は何もメッセージが表示されていない
- コメントを入力せずに「Aさんに送る」ボタンを押すと「コメントがありません。」と表示される
- コメントを入力せずに「Bさんに送る」ボタンを押すと「コメントがありません。」と表示される
- コメントを入力して「Aさんに送る」ボタンを押すと「Aさんに『
コメント
』を送りました。」と表示される - コメントを入力して「Bさんに送る」ボタンを押すと「Bさんに『
コメント
』を送りました。」と表示される - メッセージが表示されているときに違うコメントを入力すると、メッセージが消える
以上の項目を実装したテストがこちら。
まずコンポーネントをマウントし、VueWrapper.find メソッドで data-testid
属性の付与された要素を取得します。
const elemComment = wrapper.find('[data-testid="input-comment"]')
const elemSendA = wrapper.find('[data-testid="button-send-a"]')
const elemSendB = wrapper.find('[data-testid="button-send-b"]')
const elemMessage = () => wrapper.find('[data-testid="message"]')
elemComment
が入力フィールド、elemSendA
が「Aさんに送る」ボタン、elemSendB
が「Bさんに送る」ボタン、elemMessage
がメッセージ表示エリアの要素となります。elemMessage
だけが関数になっているのは、v-if
で要素が消える可能性があるためです。
await elemSendA.trigger('click')
テスト内でボタンを押下するためには VueWrapper.trigger メソッドで click
イベントを送ります。
await elemComment.setValue('こんにちは')
テスト内で入力フィールドに文字列を入力するためには VueWrapper.setValue メソッドを使います。
setValue や trigger を使うときは、VueがDOMを更新するまで待つために、awaitする必要があります。詳しくはこちら。
props と emit のテスト
ほとんどのコンポーネントでは、親コンポーネントから送られるプロパティや、親コンポーネントにイベントを送る emit があると思いますので、そのテストを書きます。
コンポーネント作成
このようなコンポーネントを作成します。
- 受信者は props で受け取ります。
- コメントを書くとリアルタイムでメッセージに反映されます。
- 送信ボタンを押すとコメントが emit されます。
コンポーネントのソースはこちら。
-
receiver
は受信者を受け取るプロパティです。 -
comment
は入力フィールドに入力した文字列です。 -
typingMessage
入力中のメッセージで、computed によってリアルタイムに反映されます。 -
sendComment
は送信ボタンを押したときに呼び出される関数で、sendComment
イベントを emit します。
テストを実装する
このフォームのテストではどのようなテスト項目が必要かを考えてみます。
- props で渡された受信者が送信ボタンに反映されていること
- 最初はメッセージが空であること
- コメント入力中は、入力中のメッセージが表示されること
- 送信ボタンを押すとコメントが emit されること
以上の項目を実装したテストがこちらです。
const wrapper = shallowMount(MyForm2, {
props: { receiver: '鈴木' }
})
コンポーネントをマウントするときにオプションで props を渡します。
const sendEvent = wrapper.emitted('sendComment')
sendComment
イベントの emit 結果を取得しています。
VueWrapper.emitted で emit された情報を取得できます。
expect(sendEvent).toHaveLength(1)
emit は複数回呼ばれる可能性があるので、emitted の戻り値は配列となります。
こちらで1回しか呼ばれていないことをテストしています。
expect(sendEvent?.[0]).toEqual(['こんばんは'])
emit したコメントの内容をテストしています。
emitted は undefined を戻す可能性があるので、型エラーを防ぐためにオプショナルチェーンでアクセスしています。
子コンポーネントのスタブ化
例えば、親コンポーネントが子コンポーネントを呼び出していて、子コンポーネントで外部へのAPIアクセスなどがある場合、子コンポーネントをスタブ化しておくと単体テストがやりやすくなる場合があります。
コンポーネント作成
このようなコンポーネントを作成します。
親コンポーネントのソースはこちら。
子コンポーネントのソースはこちら。
- 子コンポーネントに props で入力フィールドの値を渡します。
- 子コンポーネントで渡された値を2倍し、メッセージを表示します。
テストを実装する
- 親コンポーネントと子コンポーネントの両方をマウントする
- 親コンポーネントのみマウントし、子コンポーネントはスタブとする
この2種類のテストを実装してみます。
const MyForm3ChildStub = {
props: {
number: {
type: Number,
required: true
}
},
template: '<div>{{ number }}を2倍すると{{ number * 2 }}です</div>'
}
スタブコンポーネントです。元の MyForm3Child
コンポーネントとほぼ同じ機能なのであまりスタブとは言えないかも知れませんが、このようにコンポーネントをまるっと入れ替えることもできます。
const wrapper = mount(MyForm3, {
global: {
stubs: {
MyForm3Child: MyForm3ChildStub
}
}
})
mount のオプションで指定することでスタブ化できます。
Jest の mock 機能を使ってみる
Vueファイルでない場合は、Jest の mock 機能を使うと便利です。
コンポーネント作成
このようなコンポーネントを作成します。
コンポーネントのソースはこちら。
- 入力フィールドの値を
double
関数を呼び出して2倍にし、メッセージを表示します。
double
関数のソースはこちら。
テストを実装する
double 関数をモックにしたテストを実装します。
jest.mock('@/components/double')
double.ts
をモック化しています。
afterEach(() => {
jest.resetAllMocks()
})
モックの呼び出し回数や戻り値のセットはテスト間でリセットされないので、resetAllMocks で毎回リセットしています。
(double as jest.Mock).mockReturnValue(6)
モック化した double 関数の戻り値を mockReturnValue によって 6
にセットしています。その際、double 関数には mockReturnValue プロパティは生えていないので、型エラーが出ないように型アサーションしています。
expect(double).toHaveBeenCalledTimes(2)
toHaveBeenCalledTimes で double 関数の呼ばれた回数をテストしています。マウント時と setValue 時で2回呼ばれます。
expect(double).toHaveBeenLastCalledWith(3)
toHaveBeenLastCalledWith で最後に double 関数が呼ばれたときの引数をテストしています。
参考文献
- Vue Testing Handbook: Vueのテストについての素晴らしいハンドブックです。
- なぜJestのmockライブラリに混乱してしまうのか?: Jest のモック機能についての大変わかりやすい記事です。
Discussion