はじめに
こんにちは!
SHIFT 開発エンジニア兼テスト自動化アーキテクトの矢坂(ヤサカ)です。
現在、機会に恵まれて、はじめてVue.jsを使ってフロントエンド開発をしています。慣れてくると書き心地がよく、もっと人気出てもいいのになーと感じる今日この頃です。
最近は、Vue Testing Libraryを用いたテストコードも実装しています。
その中で、Vue.jsのTransitionを使用したUIコンポーネントに対してテストコードを実装する機会がありました。
Vue Testing LibraryでのTransitionのテストコード実装方法について述べている情報源が見つけられず、解決までにそこそこ試行錯誤したので、今回Tipsとして共有しようと思います。
TL; DR
-
Vue Testing Libraryのrenderは、デフォルトでTransitionをモックする
-
ただし、Transitionの「JavaScript Hooks」はモックのサポート対象外
-
「JavaScript Hooks」を使用する場合、renderでTransitionをモックしないように指定する(以下)
render(Component, {
global: {
stubs: {
transition: false,
},
},
})
動作環境
以降でご説明するコードは、以下の環境で動作することを確認済です。
前提知識:Transitionとは
そもそもTransitionとは何ぞや?というところですが。
Transitionは、Vue.jsに最初から用意されている(組み込みの)UIコンポーネントの一つです。
HTML要素やUIコンポーネントが表示された・非表示になった等の、状態の変更に伴うアニメーションを実装するための機能となります。
アニメーション適用のための専用のCSSクラスの他、JavaScriptによる制御も可能で、アニメーション開始/終了時に実行する処理を定義できます。
より詳しい説明は、公式ドキュメントを参照ください。
トランジション | Vue.js
UIコンポーネントとしてアニメーション制御の仕組みを提供している点が特徴的ですね😎
「通常のUIコンポーネントと同じように扱える」というのも、Vue.jsの設計思想が現れているようで、個人的に面白いな~と感じています。
解説用の題材
なるべく単純な機能である方がわかりやすいかと思うので、
「ボタンを押したら文字列がフェードインしたのち表示される」
という機能を持ったUIコンポーネントを、本記事での解説用題材とします。

Transitionを使用すると、以下のように実装できます。
※実装コードはVue SFC Playground から確認可能です。実際に動かしてみることもできるので、興味のある方は試してみてください!
"onAfterEnter">
"isShown">
{{ showingText }}
コード内の「
Transitionで指定している「@after-enter」はJavaScript Hooks と呼ばれるもので、JavaScriptによるアニメーション制御の仕組みに該当します。
実装例の「@after-enter」では、子要素の出現アニメーション完了時に行われる処理を指定できるため、
-
「fade in」ボタンをクリック
-
「Now fade in…」テキストがフェードインのアニメーションをしながら出現
-
アニメーション完了後、「@after-enter」に指定していた「onAfterEnter」関数が実行され、テキストが「Completed.」へ変更される
という流れで、題材として挙げた機能を実現できています。
テストコード実装例
先ほどの実装コード(App.vue)に対して、Vue Testing Libraryを使用して下記二つのユーザーインタラクションを想定したテストコードを書いてみましょう。
-
ボタンのクリック前は、テキストが非表示である
-
ボタンのクリック後は、テキストが表示される
実装例を示します。
import { userEvent } from '@testing-library/user-event'
import { render, screen } from '@testing-library/vue'
import App from '@/App.vue'
test('1.「fade in」ボタンをクリックしていないと、「Completed.」テキストは非表示である', async () => {
render(App)
const text = screen.queryByRole('heading', {
name: /Completed./,
level: 2,
})
expect(text).not.toBeInTheDocument()
})
test('2.「fade in」ボタンをクリックすると、「Completed.」テキストが表示される', async () => {
render(App)
const button = screen.getByRole('button', { name: /fade in/ })
await userEvent.click(button)
const text = await screen.findByRole('heading', {
name: /Completed./,
level: 2,
})
expect(text).toBeVisible()
})
実装したテストコードを実行してみると、1.は成功しますが、2.は失敗します。
$ npm run test:unit
> sample-app@0.0.0 test:unit
> vitest
DEV v3.2.4 /workspace
❯ src/__tests__/App.test.ts (2 tests | 1 failed) 1119ms
✓ 1.「fade in」ボタンをクリックしていないと、「Completed.」テキストは非表示である 74ms
× 2.「fade in」ボタンをクリックすると、「Completed.」テキストが表示される 1043ms
→ Unable to find role="heading" and name `/Completed./`
(冗長なため省略)
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
FAIL src/__tests__/App.test.ts > 2.「fade in」ボタンをクリックすると、「Completed.」テキストが表示される
TestingLibraryElementError: Unable to find role="heading" and name `/Completed./`
Ignored nodes: comments, script, style
-578cc705=""
>
"false"
css="true"
data-v-578cc705=""
persisted="true"
>
-578cc705=""
style=""
>
-578cc705=""
>
Now fade in...
Ignored nodes: comments, script, style
-578cc705=""
>
"false"
css="true"
data-v-578cc705=""
persisted="true"
>
-578cc705=""
style=""
>
-578cc705=""
>
Now fade in...
❯ waitForWrapper node_modules/@testing-library/dom/dist/wait-for.js:163:27
❯ node_modules/@testing-library/dom/dist/query-helpers.js:86:33
❯ src/__tests__/App.test.ts:30:29
28|
29|
30| const text = await screen.findByRole('heading', {
| ^
31| name: /Completed./,
32| level: 2,
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files 1 failed (1)
Tests 1 failed | 1 passed (2)
Start at 22:55:51
Duration 2.21s (transform 134ms, setup 121ms, collect 262ms, tests 1.12s, environment 404ms, prepare 141ms)
2.のテストも成功させるには、renderの第二引数を以下のように指定します。
render(App, {
global: {
stubs: {
transition: false, // global stubs transition = false
},
},
})
上記の修正により、2.のテストも成功するようになります。
$ npm run test:unit
> sample-app@0.0.0 test:unit
> vitest
DEV v3.2.4 /workspace
✓ src/__tests__/App.test.ts (2 tests) 168ms
✓ 1.「fade in」ボタンをクリックしていないと、「Completed.」テキストは非表示である 87ms
✓ 2.「fade in」ボタンをクリックすると、「Completed.」テキストが表示される 80ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 23:01:21
Duration 1.29s (transform 136ms, setup 119ms, collect 326ms, tests 168ms, environment 410ms, prepare 102ms)
テストの失敗原因
最初に実装した2.のテストは、なぜ失敗したのでしょうか。
Vue Testing Libraryの公式ドキュメントを参照すると、render関数の説明書きがあります。
render(Component, options)
An object containing additional information to be passed to @vue/test-utils mount .
上記の「@vue/test-utils」はVue Test Utils を指し、これはVue.js公式のテスト用ライブラリです。
つまりrender内部では、Vue Test Utilsの提供するmount関数を、指定したオプション引数を渡した上で呼び出します。
さて、このmountですが、デフォルトではTransitionをモックしてUIコンポーネントの描画をシミュレートします。
ただし、Transitionのすべての機能をモックできるわけではなく、「JavaScript Hooksはサポートしていない」と明言されています。
Partial support
The Vue Test Utils built-in transition stub is simple and doesn’t cover all of Vue’s Transition features . For instance javascript hooks are not supported. This limitation could potentially lead to Vue warnings.
今回題材にしているUIコンポーネントの場合、先述したようにTransitionで「@after-enter」というJavaScript Hooksを使用しています。
したがって、2.のテストは、
-
テスト実行時にTransitionの「JavaScript Hooks」を含めてモックしている
-
モックにより、「@after-enter」に指定した処理(上記例ではonAfterEnter関数)が実行されない
-
結果、「fade in」ボタンをクリックしても「Completed.」テキストが表示された状態にならない(非表示のまま)
という理由で失敗した、というわけです。
対処方法
ではどうすればよいのか?というと・・・
Vue Test Utilsの公式ドキュメント(先ほどの引用箇所の下)に『global stubs transitionへfalseを設定することで(Transitionを)モックしないようにできる』と、解決方法が記載されています。
TIP
Potential solutions:
・You can turn off the auto stubbing by setting global stubs transition to false
・You can create your own transition stub that can handle these hooks if necessary.
・You can spy the warning in the test to silence it.
ここでいう「global stubs transition」は、mountに指定可能なオプション引数の一つです。
前述の通り、renderは指定したオプション引数を渡した上でmountを呼び出すので、
-
renderに「global stubs transition = false」を指定すると、それがそのままmountに渡される
-
上記指定により、「JavaScript Hooks」も含めてTransitionがモックされなくなる
-
結果、UIコンポーネントが実装した通りの振る舞いをする
ようになります。
修正後のテストコードが成功するようになったのは、このトリックによって「@after-enter」が働くようになり、(実装した通りに)テキスト変更が行われたため、というのが真相でした。因果関係がわかってスッキリ。
おわりに
本記事では、題材コンポーネントを基に、Transition使用時のVue Testing Libraryによるテストコード実装のTipsを共有させて頂きました。皆様のご参考になれば幸いです。
今回のテスト失敗原因の調査過程で、(AIの力も借りつつですが)Vue Testing Library, Vue Test Utilsの実装コードとひたすらにらめっこしてました。
それなりに時間は費やしたものの、「普段何気なく利用しているライブラリやフレームワークの実装コードを読み、構成や仕様を把握する」という経験ができたのは良かったな~と思っています。
特に、Vue Testing Libraryの実装は学びがありました。「HTML要素検索用のクエリ関数群」と「Vue Test Utilsの提供API」をうまいことラップし、テストコード実装者が扱いやすい、極力シンプルなAPI設計になっているように感じました。私もスマートな設計がシュッとできるように、今後も精進していきます💪
最後まで読んで頂きありがとうございました!
余談:テストコード実装例をPull Requestしてみた
当記事の公開時点では、Vue Testing LibraryのGitHubリポジトリには参考となるテストコード実装例はないようでしたので、私の方でPull Requestを出してみました。
https://github.com/testing-library/vue-testing-library/pull/327
Approveされると嬉しいですが、気長に待ちましょう🍵
執筆者プロフィール:矢坂 拓
2025年1月に株式会社SHIFTへ入社。
開発エンジニア兼テスト自動化アーキテクトやってます💪
積本が減るどころかどんどん増えるバグ🐞が一生直りません。
_(:3」∠)_←この顔文字が好き。
✉ この記事の執筆者へ感想を届けてみませんか?🖊
「役に立ちそう!」「参考になった!」「思考が整理できた」など
記事を読んだ感想をぜひ、アンケートにてお寄せください。
次回の執筆テーマの参考にさせていただきます!
ご質問も大歓迎です◎★読者アンケートはこちら
★本記事のコメント欄でも受け付けております
✅SHIFTへのお問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/
Source link
コメント