監視の上で、たとえばまずこのサイトにアクセスして、次にログインをして、その結果レスポンスボディはこうあってほしい、というシナリオテストをしたいことがある。あるいはAPIサーバー相手だと、最初にエンドポイントにこのパラメータ付きでアクセスし、次にこのエンドポイントを叩き、結果JSONはどうだ、というケースもある。
Mackerelの外形監視は「URLエンドポイントにシンプルなHTTPライブラリを使ってアクセスしてレスポンスボディを受け取って判定する」という仕組みなので、この手のシナリオ監視ができない。
APIサーバー相手ではrunnを使ったmackerunn(いい名前!)を arthur-1 さんが作っていた。
これに対し、「全部JavaScriptで作ってます」みたいなSPAサイトでは、これらの方法ではうまくいかず、JavaScriptでページを作る本物のブラウザが必要になる。URLにアクセスして、現れたフォームに入力したりボタンをポチったりするなどして、レスポンスボディが動的生成され終わるのを待ち、その中で特定の要素内に求めているものがあるかを検査するという類いである。
この手のE2Eテストツールとしては、かつてはPuppeteer、今はPlaywrightが主流である。
…というどこにでも転がってそうな話を今さら語る前置きから、実際Playwrightの結果に基づいてMackerelにmkr wrapを使ってアラートを出すことをやってみた。
対象アプリケーションを用意する
テスト用に、ログインフォームと、ログイン後にHelloメッセージを表示するだけのWebサーバーを用意。
package.json
{
"name": "app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"body-parser": "^2.2.0",
"express": "^5.1.0"
}
}
app.js
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const PORT = 3000;
// 中間ウェア設定
app.use(bodyParser.urlencoded({ extended: true }));
// ログインフォーム
app.get('/', (req, res) => {
res.send(`
<form action="/login" method="post">
<label>Username: <input name="username" /></label><br/>
<label>Password: <input type="password" name="password" /></label><br/>
<button type="submit">Login</button>
</form>
`);
});
// ログイン処理
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username === 'user' && password === 'pass') {
res.send(`<h1>Hello, ${username}!</h1>`);
} else {
res.send('<h1>Login failed</h1>');
}
});
// サーバー起動
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
npm install、node app.jsでポート3000で起動する。
Playwrightテストを書く
次にPlaywrightテストを用意する。
package.json
{
"name": "play",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"playwright": "^1.52.0"
},
"devDependencies": {
"@playwright/test": "^1.52.0"
}
}
playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [['line']],
});
reporterのlineは、実行ログ出力を最小限にする指示。出力内容は後々Mackerelのアラート内容にも使われるので、アラート用途に対して余計な情報は最小限にしておきたい。
npm installし、さらにnpx playwright install chromium --with-depsでChromiumブラウザをインストールした(ヘッドレスで動かす何か1つのブラウザは必要)。
テストコードは全部手で書くこともできるが、npx playwright codegenを実行すると空のブラウザが開くので、実際の操作をしながら記録していける。

タブキーでの移動や誤った操作などは実際のコードでは不要なので、適宜調整する。
login.spec.tsという名前にしてみた。
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('http://localhost:3000/');
await page.getByRole('textbox', { name: 'Username:' }).fill('user');
await page.getByRole('textbox', { name: 'Password:' }).fill('pass');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByRole('heading', { name: 'Hello, user!' })).toBeVisible();
});
userとpassを入れてLoginボタンクリック、見出しに「Hello, user!」が表示されることを期待するというテスト。
npx playwright testで実行する。
% npx playwright test Running 1 test using 1 worker 1 passed (781ms)
よさそうだ。
パスワードをpassじゃないのにしてみる。
% npx playwright test
Running 1 test using 1 worker
1) login.spec.ts:3:5 › test ──────────────────────────────────────────────────
Error: Timed out 5000ms waiting for expect(locator).toBeVisible()
Locator: getByRole('heading', { name: 'Hello, user!' })
Expected: visible
Received: <element(s) not found>
Call log:
- expect.toBeVisible with timeout 5000ms
- waiting for getByRole('heading', { name: 'Hello, user!' })
6 | await page.getByRole('textbox', { name: 'Password:' }).fill('pass2');
7 | await page.getByRole('button', { name: 'Login' }).click();
> 8 | await expect(page.getByRole('heading', { name: 'Hello, user!' })).toBeVisible();
| ^
9 | });
10 |
at .../play/login.spec.ts:8:69
Error Context: test-results/login-test/error-context.md
1 failed
login.spec.ts:3:5 › test ───────────────────────────────────────────────────
エラーレポートとしてtest-results/login-test/error-context.mdというファイルができるとともに、端末にもその内容が表示された。
mkr wrapで実行結果を報告する
mkrはMackerelのAPI実行ツールで、mkr wrapオプションは引数指定のコマンドの実行結果の成否をあたかもチェック監視のように報告するというものである。実行に失敗(終了コードが0でない)したらアラートが発生する。詳細例は公式ドキュメントに詳しい。
npx playwright testも成否の終了コードを返すので、単にそれを使えばよい。
(MACKEREL_APIKEY=APIキー mkr wrap -n playwright-hello -d -H ホストID -a -- npx playwright test) >/dev/null
- APIキー:Mackerelオーガニゼーションのキー
-n:チェック監視名-d:コマンド実行結果を内容に含める-H:ホストIDの指定。チェック監視として紐づけるホストのID。省略したら自ホスト-a:既報のアラートがあり、今回の実行で成功したらアラートをクローズする
そのまま実行すると出力がうるさいので()で囲んで/dev/null送りにしている。

それっぽい結果になっている。エスケープシーケンスが文字化けになっているのはご愛嬌。TERM変えたら出ないようにできたりするのかな。
コンテナ化の夢を見るか
SaaSを使うからには監視のためにサーバー運用はしたくないのだろうから、Lambdaで動かせるか考えてみた。
mcr.microsoft.com/playwright:v1.44.0-focal Dockerイメージをベースにchromiumブラウザも入れてコンテナイメージを作ってみたところ、3GBにもなってしまった。Lambdaで動かすには起動まで時間がかかりすぎてあまり現実的でなさそうだ。
Lambdaではなく、EC2なりECSなりにして定期実行するのが妥当かな。
また、今回は最低限出力モードにしていたが、本来はPlaywrightでもっとリッチなレポートを出せる。しかしMackerelのチェック監視の情報粒度では別途見にいかないといけない。単純にSPAのページで何かが出るのを確認するといった程度ならともかく、細かく結果を見ることも運用に含んでいるなら、専門SaaS型のChecklyあたりを使うのがよさそうかなという気がした。