JestとPuppeteerを使ってフロントエンドにE2Eテストを導入する方法
フロントエンドの挙動を確認する際にいちいちブラウザで操作はするのは面倒です。
テストを自動化すれば面倒な作業に費やしていた時間を新しい機能の開発にあてることができます。
以下のツールを使ってE2Eテストを導入してみたので今回はその際のテストコードや解説を書いていきたいと思います。
- Jest:JavaScriptテスティングフレームワーク
- Puppeteer:ヘッドレスChromeブラウザ
E2Eテストとは End to End のことでユーザー操作によるアプリケーションの挙動を検証するためのテスト手法です。
例えば初期表示で対象のエレメントが表示されているか、新規追加を行うと一覧に追加されるか、削除を行うと一覧から削除されるか等を検証するためのテストです。
テスト対象の機能
例としてVue.jsで作られた単純なタスク管理アプリのフロントエンドの機能についてテストします。
主な機能としては以下の3つです。
- タスクの追加、更新
- タスクの一覧表示
- タスクの削除
コンポーネントは以下のようになっています。
TodoApp.vue
<div>
<button id="addTodo" @click="addTodo">タスクを追加する</button>
<todo-list
:visible="visible"
:todo-list="todoList"
@updateTodo="updateTodo"
@removeTodo="removeTodo"
/>
<todo-empty :visible="!visible" />
<div>
<next-todo :next-todo-text="nextTodoText" />
<todo-count :count="count" />
</div>
</div>
methods: {
updateTodo(payload: { index: number; value: string }) {
mutations.updateTodo(payload.index, payload.value);
},
addTodo() {
mutations.addTodo();
},
removeTodo(payload: { index: number }) {
mutations.removeTodo(payload.index);
},
},
TodoList.vue
<div id="todoList" v-show="visible">
<div v-for="(todo, index) in todoList" :key="todo.key" class="todo">
<input type="text" @input="updateTodo(index, $event.target.value)" />
<button class="delete" @click="removeTodo(index)">削除</button>
</div>
</div>
TodoEmpty.vue
<div id="todoEmpty" v-show="visible">タスクがありません</div>
NextTodo.vue
<span id="nextTodo">次のTODO: {{ nextTodoText }}</span>
TodoCount.vue
<span id="todoCount">(全{{ count }}件)</span>
状態管理はStore.tsというファイルを用意して以下のように定義しています。
const store = Vue.observable({
todoList: [] as Todo[],
nextTodoText: "",
todoCount: 0,
});
export const mutations = {
addTodo() {
store.todoList.push({
key: new Date().getTime(),
todo: "",
});
},
removeTodo(index: number) {
store.todoList.splice(index, 1);
},
updateTodo(index: number, value: string) {
store.todoList[index].todo = value;
},
};
ライブラリのインストール
npm を使って jest, puppeteer, jest-puppeteer などのテスト用のパッケージをインストールします。開発用のライブラリなので -D でインストールするのが良いです。
バージョンによってはテストコードがうまく動作しないのでインストール時はバージョン指定が必要です。パッケージのバージョン一覧はこのようになっています。
"devDependencies": {
"@types/expect-puppeteer": "^3.3.1",
"@types/jest": "^24.0.13",
"@types/jest-environment-puppeteer": "^4.0.0",
"@types/jest-image-snapshot": "^2.8.0",
"@types/node": "^12.0.2",
"@types/vue": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^1.9.0",
"css-loader": "^2.1.1",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^4.3.0",
"eslint-plugin-import": "^2.17.2",
"eslint-plugin-prettier": "^3.1.0",
"jest": "^24.8.0",
"jest-image-snapshot": "^2.8.2",
"jest-puppeteer": "^4.1.1",
"prettier": "^1.17.1",
"puppeteer": "^1.16.0",
"style-loader": "^0.23.1",
"ts-jest": "^24.0.2",
"ts-loader": "^6.0.1",
"typescript": "^3.4.5",
"vue-loader": "^15.7.0",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.32.2",
"webpack-cli": "^3.3.2"
},
"dependencies": {
"vue": "^2.6.10"
}
Jest設定ファイル
プロジェクトのルートフォルダに以下の内容で jest.config.js というファイルを作成します。preset を指定することにより jest-puppeteer が有効になります。
module.exports = {
preset: "jest-puppeteer",
transform: {
"^.+\\.tsx?$": "ts-jest",
},
moduleFileExtensions: ["ts", "js"],
};
jest-puppeteer.config.js を作成することで jest-puppeteer の設定を行うことも可能です。
例えば以下のように書くとテストコードを動かす際に実際にブラウザを起動して挙動を目視できます。
module.exports = {
launch: {
headless: false,
},
};
テストコード
spec/e2e.spec.ts というファイルを作成しています。最終的なコードは以下の通りです。
import path from "path";
describe("TODOアプリ", () => {
beforeEach(async () => {
await page.goto("file://" + path.resolve(__dirname, "../index.html"));
await page.waitForSelector("#todoList", { visible: false });
await page.waitForSelector("#todoEmpty", { visible: true });
});
it("初期表示", async () => {
await expect(page).toMatchElement("#todoEmpty", {
text: "タスクがありません",
});
await expect(page).toMatchElement("#nextTodo", {
text: "次のTODO: (未登録)",
});
await expect(page).toMatchElement("#todoCount", {
text: "(全0件)",
});
});
it("タスクの追加", async () => {
await page.click("#addTodo");
await page.waitForSelector("#todoEmpty", { visible: false });
await expect(page).toMatchElement("#todoCount", {
text: "(全1件)",
});
await page.click("#addTodo");
await expect(page).toMatchElement("#todoCount", {
text: "(全2件)",
});
});
it("タスクの入力", async () => {
await page.click("#addTodo");
await page.click("#addTodo");
await page.type(".todo:nth-child(1) input", "サンプルタスク1");
await page.type(".todo:nth-child(2) input", "サンプルタスク2");
await expect(page).toMatchElement("#nextTodo", {
text: "次のTODO: サンプルタスク1",
});
});
it("タスクの削除", async () => {
await page.click("#addTodo");
await page.click("#addTodo");
await page.type(".todo:nth-child(1) input", "サンプルタスク1");
await page.type(".todo:nth-child(2) input", "サンプルタスク2");
await page.click(".todo:nth-child(1) .delete");
await expect(page).toMatchElement("#nextTodo", { text: "サンプルタスク2" });
await page.click(".todo:nth-child(1) .delete");
await page.waitForSelector("#todoList", { visible: false });
await page.waitForSelector("#todoEmpty", { visible: true });
await expect(page).toMatchElement("#todoCount", { text: "(全0件)" });
});
});
前準備
beforeEach
は非同期コードのテストをするために必要です。
page.goto()
を使って対象の画面を開きます。今回はローカルファイルを指定しています。
page.waitForSelector()
を使って対象セレクターの有無をチェックしています。
初期表示
expect(page).toMatchElement()
を使って項目が正しく表示されているのかをテストできます。
ユーザー操作
page.click()
で指定したエレメントをしてクリックします。
page.type()
でテキストボックスに値を入力します。
テストの実行
以下のコマンドで上記のテストコードを走らせることができます。npx を使って node_modules 内の jest コマンドを呼び出しています。
npx jest ./spec/e2e.spec.ts
コンソールの表示は以下のようになります。(4件ともテストが通っている例)
% npx jest ./spec/e2e.spec.ts
PASS spec/e2e.spec.ts
TODOアプリ
✓ 初期表示 (379ms)
✓ タスクの追加 (75ms)
✓ タスクの入力 (135ms)
✓ タスクの削除 (115ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 2.731s
Ran all test suites matching /.\/spec\/e2e.spec.ts/i.
package.json の script にコマンドを書いておければ短縮コマンドでテストの実行も可能です。
"scripts": {
"test": "jest spec/e2e.spec.ts",
テストの実行
npm test
まとめ
テストコードの導入はそれなりに手間がかかります。テストライブラリの依存関係の考慮、テスト項目の検討、テストコードの実装などを行う必要があるからです。
それでも導入してしまえばフロントエンドの挙動を保証しながら開発を進められるので非常にありがたい存在です。
今回は簡単なアプリでのテストコードの実装方法について解説しましたので導入する際の参考にでもなれば幸いです。
参考
迷わない!困らない!レガシーフロントエンド安全改善ガイド (技術の泉シリーズ(NextPublishing)) Kindle版
Discussion