はじめに
こんにちは。弊社ではエンジニアの所属部門と離れた社内活動として、IT委員会という組織があります。IT委員会は分科会という所掌する分野ごとに分かれており、社内のシステム開発において採用すべき技術標準の策定や、守るべきセキュリティ標準などのガイドライン作成と情報展開などを行っています。
この記事では、ソフトウェアテストを担当する分科会より、フロントアーキテクチャとしての弊社内標準であるNuxtを利用したサイトのUI部分の自動ブラウザテストを行う方法のアイデアを、サンプルアプリケーションの実装で紹介します。テストコードは、CIによって繰り返し実行されるような目的のテストを想定しています。
社内向けガイドラインでは自動テストのメリットや目的などの説明を前書きとして書くのですが、今回はブログでの公開ということもあり、割愛させていただきます。
サンプルアプリケーション構造
外部APIサーバーに用意されたHelloWorld APIの結果を画面に表示するだけの単純なページを想定します。
セットアップとページ作成
公式手順に従って、Nuxt31 をセットアップします。
npx nuxi init nuxt-app cd nuxt-app yarn install
ページを作ります。useFetch()を使って外部サーバーからHelloWorldの結果を表示します。きっと各国の言葉で返ってくるパターンがあるのでしょう。
pages/index.vue
<template> <div> <template v-if="error"> <p>エラー:{{ error }} </p> </template> <template v-else> <p> 言語:{{ data?.lang }} </p> <p> 言葉:{{ data?.word }} </p> </template> </div> </template> <script setup lang="ts"> const helloworld = ref<{ lang: string, word: string }>(); const { data, error } = await useFetch<typeof helloworld>("https://url-of-apiserver/helloworld"); </script>
app.vueの<NuxtWelcome />を<NuxtPage />に変更します。
app.vue
<template> <div> <NuxtPage /> </div> </template>
テストツール準備
ブラウザでのテストにはPlaywrightを使います。
yarn add -D @playwright/test
インストールしたら動作設定をします。ブラウザの動きが見たいのでheadless: falseにして、expectのタイムアウト値を10秒にしています。サーバーを立ち上げた後の初回アクセス時に待ち時間があるためです。
playwright.config.ts
import type { PlaywrightTestConfig } from "@playwright/test"; const config: PlaywrightTestConfig = { use: { headless: false, ignoreHTTPSErrors: true, browserName: "chromium", screenshot: "on" }, testDir: "tests", outputDir: "tests-output", expect: { timeout: 10000 } }; export default config;
テストコード
テストを実装します。Playwrightにはブラウザ→サーバーへの通信をモックする機能があります。正常系では英語でのHelloWorldが、異常系では403エラーになるようにしています。
localhostのindexページが表示されたら、API結果による画面内容の変化を評価します。本来はlocatorを駆使して変化する場所を狙って確認するところですが、ここでは大雑把にページのどこかに期待する結果が出ていることの確認とします。
tests/pages/index.spec.ts
import { test, expect } from "@playwright/test"; test.describe("testing with api-call-mock", () => { test("api responds in english", async ({ page }) => { await page.route("**/helloworld", route => { route.fulfill({ status: 200, contentType: "application/json", body: '{"lang":"english","word":"helloworld"}' }) }); await page.goto("http://localhost:3000"); await expect(page.locator("body")).toContainText("言語:english"); await expect(page.locator("body")).toContainText("言葉:helloworld"); }); test("api responds an error", async ({ page }) => { await page.route("**/helloworld", route => { route.abort("accessdenied"); }); await page.goto("http://localhost:3000"); await expect(page.locator("body")).toContainText("エラー"); }); });
モードによる動作の違い
上記のテストはUniversal Renderingモード(SSR)では必然的に失敗します。Universal Renderingではページが表示される前にNuxtサーバーから外部APIサーバーへのサーバー間通信が実行されますが、同じAPIが同じ入力で同時に正常な結果とエラーを返すことはあり得ないため、ケースが相互に矛盾することになるからです。2
また別の課題として、外部サーバーへアクセスするとネットワークその他のアプリケーション外の原因でテストが失敗する可能性が生まれるため、特にCIで繰り返し実行するような自動化テストではそのようなノイズをできる限り排除したいところです。
従って、APIの結果による画面表示内容を関心ごととしている今回のアイデアでは、モックを使うためにClientモード(SPA)でNuxtを起動するということが条件になります。3
nuxt.config.ts
export default defineNuxtConfig({ ssr: false })
テストの実行方法
このテストの実行は以下の手順が必要です。
- NuxtサーバーをClientモードで起動する。
- Playwrightテストを実行する。
- Nuxtサーバーを停止する。
バックグラウンドでサーバーを立ち上げる必要があるため、今回はpm2を使用しました。まずはインストールします。
yarn global add pm2
Nuxtの起動設定を記述します。
ecosystem.config.js
module.exports = { apps: [ { name: 'NuxtPlaywright', exec_mode: 'cluster', instances: '1', script: './node_modules/nuxt/bin/nuxt.mjs', args: 'dev' } ] }
順番通りにscriptを追加します。テストが失敗するとサーバーが停止しないので、exit 0を記述しておきます。
package.json
"scripts": { ~略~ "pretest": "pm2 start", "test": "npx playwright test || exit 0", "posttest": "pm2 stop NuxtPlaywright" }
実行します。
yarn test
screenshotをonにしておけば、実行時の画面キャプチャを後で確認することができます。
nuxt/test-utils
公式のテストツールがあります。こちらはvue-test-utilsと同様のpageやcomponent単位のUnitテストとPlaywrightを利用したブラウザテストの両方ができるようです。後者の場合はツール内部で自動的にUniversal RenderingモードでNuxtを起動するようで、サーバー間通信もそのまま実行されます。おそらくE2Eテストが主目的なのだろうと思います。既に説明した通り、本記事ではUI部分のCI実行を目的としているため別のアプローチとしましたが、E2Eテストを自動化する場合には有用なソリューションになると思われます。