k6導入 ~ k6 browserでE2Eテストまでにやってきたことのまとめ
はじめに
zenn初めてみました✋
スカイウイルでインフラエンジニアをしております。
案件でk6の調査/実装をする機会があったのでまとめてみました。
中でもk6 browserは実験的なモジュールということもあってか関連する記事が少ないため、今回の記事が役に立てればと思います。
k6とは
パフォーマンステストおよび負荷テストのためのオープンソースツールであり、Webアプリのパフォーマンスを評価するために利用できます。
以下のような特徴があります
- 並列実行が可能
- JavaScriptでテストシナリオを記述
- CLI
- 外部統合の容易さ
- グラフィカルなレポート生成
- Goで作られている
仕組みとしては、webアプリにAPIテストをしてボトルネックを計測することができます。
これにより、バージョン間で遅くなったAPIの特定やテストに失敗した場合は、不具合やデグレに気づくことができます。つまり、CIに組み込んで使うとよりメリット享受できるかと思います。
これだけであればJmeterあたりでも実現そうですが、特に並列実行できることが強み🔥です。
環境
- docker 26.0.1 (docker Engine)
- docker-compose v2.4.1
今回のテスト対象サービス
私の自宅鯖で運用している過疎Misskeyです。
k6をコンテナで
私はdocker-compose
使わないと気が済まない性なので、まずは以下ファイルを用意します。
FROM grafana/k6:0.51.0
WORKDIR /home/k6
USER root
version: "3"
services:
k6:
build:
context: k6
image: k6:latest
container_name: k6
hostname: k6
volumes:
- ./k6/scenarios:/home/k6/
tty: true
entrypoint: ash
サンプルのテストシナリオを用意
import http from "k6/http";
export default function () {
http.get("https://pekoraskey.melanmeg.com/");
}
ディレクトリ構成
.
├── docker-compose.yml
└── k6
├── Dockerfile
└── scenarios
└── test1.js
テスト実行
$ docker-compose build
$ docker-compose up -d
$ docker-compose exec k6 k6 run test1.js
実行結果
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: test1.js
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
* default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)
data_received..................: 6.3 kB 11 kB/s
data_sent......................: 468 B 846 B/s
http_req_blocked...............: avg=365.59ms min=365.59ms med=365.59ms max=365.59ms p(90)=365.59ms p(95)=365.59ms
http_req_connecting............: avg=169.51ms min=169.51ms med=169.51ms max=169.51ms p(90)=169.51ms p(95)=169.51ms
http_req_duration..............: avg=187ms min=187ms med=187ms max=187ms p(90)=187ms p(95)=187ms
{ expected_response:true }...: avg=187ms min=187ms med=187ms max=187ms p(90)=187ms p(95)=187ms
http_req_failed................: 0.00% ✓ 0 ✗ 1
http_req_receiving.............: avg=135.28µs min=135.28µs med=135.28µs max=135.28µs p(90)=135.28µs p(95)=135.28µs
http_req_sending...............: avg=37.43µs min=37.43µs med=37.43µs max=37.43µs p(90)=37.43µs p(95)=37.43µs
http_req_tls_handshaking.......: avg=168.84ms min=168.84ms med=168.84ms max=168.84ms p(90)=168.84ms p(95)=168.84ms
http_req_waiting...............: avg=186.83ms min=186.83ms med=186.83ms max=186.83ms p(90)=186.83ms p(95)=186.83ms
http_reqs......................: 1 1.807854/s
iteration_duration.............: avg=552.99ms min=552.99ms med=552.99ms max=552.99ms p(90)=552.99ms p(95)=552.99ms
iterations.....................: 1 1.807854/s
running (00m00.6s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs 00m00.6s/10m0s 1/1 iters, 1 per VU
k6はCLIなのでサクッと試せるところが良いと思います。
出力されたレポートも見やすい。
特にみるのはhttp_req_duration
(リクエストの合計時間)
k6の並列実行について
k6にはオプションがたくさんあります。
少しこんがらがったもの以下2つについて紹介です。
-u, --vus int number of virtual users (default 1)
-i, --iterations int script total iteration limit (among all VUs)
vusが並列実行数、iterationsが反復数、つまりk6 run test1.js -u 3 -i 5
とすれば、3ユーザー同時実行を5回繰り返しますといった意味です。まあそのまんまと言えばそうですね、はい😐
結果はこんな感じ
実行結果
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: test1.js
output: -
scenarios: (100.00%) 1 scenario, 3 max VUs, 10m30s max duration (incl. graceful stop):
* default: 5 iterations shared among 3 VUs (maxDuration: 10m0s, gracefulStop: 30s)
data_received..................: 64 kB 301 kB/s
data_sent......................: 2.1 kB 9.7 kB/s
http_req_blocked...............: avg=67.63ms min=321ns med=97.44ms max=123.4ms p(90)=120.96ms p(95)=122.18ms
http_req_connecting............: avg=29.64ms min=0s med=17.73ms max=65.27ms p(90)=65.24ms p(95)=65.26ms
http_req_duration..............: avg=42.62ms min=33.54ms med=41.87ms max=49.37ms p(90)=49.14ms p(95)=49.26ms
{ expected_response:true }...: avg=42.62ms min=33.54ms med=41.87ms max=49.37ms p(90)=49.14ms p(95)=49.26ms
http_req_failed................: 0.00% ✓ 0 ✗ 5
http_req_receiving.............: avg=4.94ms min=108.39µs med=233.16µs max=23.24ms p(90)=14.35ms p(95)=18.79ms
http_req_sending...............: avg=113.34µs min=61.79µs med=93.31µs max=236.34µs p(90)=186.16µs p(95)=211.25µs
http_req_tls_handshaking.......: avg=25.18ms min=0s med=30.85ms max=58.35ms p(90)=49.7ms p(95)=54.02ms
http_req_waiting...............: avg=37.57ms min=16.22ms med=41.4ms max=49.2ms p(90)=48.58ms p(95)=48.89ms
http_reqs......................: 5 23.486128/s
iteration_duration.............: avg=110.5ms min=33.81ms med=139.59ms max=166.38ms p(90)=165.1ms p(95)=165.74ms
iterations.....................: 5 23.486128/s
running (00m00.2s), 0/3 VUs, 5 complete and 0 interrupted iterations
default ✓ [======================================] 3 VUs 00m00.2s/10m0s 5/5 shared iters
k6で色々試す
POSTリクエストして、ステータスをチェックする
ログインするテストシナリオ
import http from "k6/http";
import { check } from "k6";
export default function () {
const url = "https://pekoraskey.melanmeg.com/api/signin";
const payload = JSON.stringify({
username: "test",
password: "xxxxxxxx",
"hcaptcha-response": null,
"g-recaptcha-response": null,
});
const params = {
headers: {
"Content-Type": "application/json",
},
};
const res = http.post(url, payload, params);
const statusCheckMessage = `is status 200 for URL: ${url}`;
check(res, { [statusCheckMessage]: (r) => r.status === 200 });
}
実行結果
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: test2.js
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
* default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)
✓ is status 200 for URL: https://pekoraskey.melanmeg.com/api/signin
checks.........................: 100.00% ✓ 1 ✗ 0
data_received..................: 4.2 kB 15 kB/s
data_sent......................: 707 B 2.5 kB/s
http_req_blocked...............: avg=143.51ms min=143.51ms med=143.51ms max=143.51ms p(90)=143.51ms p(95)=143.51ms
http_req_connecting............: avg=18.17ms min=18.17ms med=18.17ms max=18.17ms p(90)=18.17ms p(95)=18.17ms
http_req_duration..............: avg=140.02ms min=140.02ms med=140.02ms max=140.02ms p(90)=140.02ms p(95)=140.02ms
{ expected_response:true }...: avg=140.02ms min=140.02ms med=140.02ms max=140.02ms p(90)=140.02ms p(95)=140.02ms
http_req_failed................: 0.00% ✓ 0 ✗ 1
http_req_receiving.............: avg=84.93µs min=84.93µs med=84.93µs max=84.93µs p(90)=84.93µs p(95)=84.93µs
http_req_sending...............: avg=626.93µs min=626.93µs med=626.93µs max=626.93µs p(90)=626.93µs p(95)=626.93µs
http_req_tls_handshaking.......: avg=84.88ms min=84.88ms med=84.88ms max=84.88ms p(90)=84.88ms p(95)=84.88ms
http_req_waiting...............: avg=139.31ms min=139.31ms med=139.31ms max=139.31ms p(90)=139.31ms p(95)=139.31ms
http_reqs......................: 1 3.523799/s
iteration_duration.............: avg=283.67ms min=283.67ms med=283.67ms max=283.67ms p(90)=283.67ms p(95)=283.67ms
iterations.....................: 1 3.523799/s
running (00m00.3s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs 00m00.3s/10m0s 1/1 iters, 1 per VU
結果をファイル出力する
k6には、カスタムメトリクスやカスタムサマリというものがあります。
単純にファイル出力するだけであれば、オプションでcsvまたはjsonで出力ができますが、
今回はスクリプト内にてjson形式で、さらに欲しいメトリクスだけを出力させてみます。
// import文追加
import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.2/index.js";
// ...
// 末尾に以下を追加
export function handleSummary(data) {
const result = {
http_req_duration: data.metrics.http_req_duration,
checks: data.metrics.checks,
};
const outputDir = "/home/k6/result.json";
return {
stdout: textSummary(data, { indent: " ", enableColors: true }),
[outputDir]: JSON.stringify(result),
};
}
{
"http_req_duration": {
"type": "trend",
"contains": "time",
"values": {
"max": 227.32455,
"p(90)": 227.32455,
"p(95)": 227.32455,
"avg": 227.32455,
"min": 227.32455,
"med": 227.32455
}
},
"checks": {
"contains": "default",
"values": {
"rate": 1,
"passes": 1,
"fails": 0
},
"type": "rate"
}
}
このように、ファイル出力できます。
標準出力にも結果を出したい場合は上記のようにtextSummaryを使う必要がありました。
テストライフサイクル、カスタムメトリクスについて
コメントを連投する
- setup()でログイン
- default function()でコメント投稿する
import http from "k6/http";
import { check } from "k6";
import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.2/index.js";
const baseUrl = "https://pekoraskey.melanmeg.com";
let date = new Date();
let now = date.toISOString().replace("T", " ").substring(0, 19);
export const options = {
scenarios: {
constant_arrival_rate: {
executor: "constant-arrival-rate",
rate: 1, // 1 iteration per timeUnit
timeUnit: "3s", // 3 seconds per iteration
duration: "10s", // test duration
preAllocatedVUs: 1, // to allocate VUs in advance
maxVUs: 1, // maximum number of VUs
},
},
};
export function setup() {
const url = `${baseUrl}/api/signin`;
const payload = JSON.stringify({
username: "test",
password: "xxxxxxxx",
"hcaptcha-response": null,
"g-recaptcha-response": null,
});
const params = {
headers: {
"Content-Type": "application/json",
},
};
const res = http.post(url, payload, params);
const statusCheckMessage = `is status 200 for URL: ${url}`;
check(res, { [statusCheckMessage]: (r) => r.status === 200 });
const token = JSON.parse(res.body).i;
return { token };
}
export default function (data) {
const token = data.token;
const url = `${baseUrl}/api/notes/create`;
const payload = JSON.stringify({
text: `test k6 comment: ${now}`,
poll: null,
cw: null,
localOnly: false,
visibility: "public",
reactionAcceptance: "nonSensitiveOnly",
i: token,
});
const params = {
headers: {
"Content-Type": "application/json",
},
};
const res = http.post(url, payload, params);
const statusCheckMessage = `is status 200 for URL: ${url}`;
check(res, { [statusCheckMessage]: (r) => r.status === 200 });
}
export function handleSummary(data) {
const result = {
api_duration: data.metrics.http_req_duration,
checks: data.metrics.checks,
};
const outputDir = "/home/k6/result.json";
return {
stdout: textSummary(data, { indent: " ", enableColors: true }),
[outputDir]: JSON.stringify(result),
};
}
実行結果
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: test3.js
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 40s max duration (incl. graceful stop):
* constant_arrival_rate: 0.33 iterations/s for 10s (maxVUs: 1, gracefulStop: 30s)
✓ is status 200 for URL: https://pekoraskey.melanmeg.com/api/notes/create
█ setup
✓ is status 200 for URL: https://pekoraskey.melanmeg.com/api/signin
checks.........................: 100.00% ✓ 5 ✗ 0
data_received..................: 12 kB 1.2 kB/s
data_sent......................: 2.2 kB 215 B/s
http_req_blocked...............: avg=40.43ms min=285ns med=449ns max=136.3ms p(90)=108.12ms p(95)=122.21ms
http_req_connecting............: avg=8.05ms min=0s med=0s max=22.53ms p(90)=20.61ms p(95)=21.57ms
http_req_duration..............: avg=90.2ms min=40.71ms med=66.17ms max=168.34ms p(90)=153.95ms p(95)=161.14ms
{ expected_response:true }...: avg=90.2ms min=40.71ms med=66.17ms max=168.34ms p(90)=153.95ms p(95)=161.14ms
http_req_failed................: 0.00% ✓ 0 ✗ 5
http_req_receiving.............: avg=94.04µs min=62.88µs med=72.48µs max=187.46µs p(90)=142.74µs p(95)=165.1µs
http_req_sending...............: avg=326.83µs min=151.55µs med=219.74µs max=644.07µs p(90)=571.73µs p(95)=607.9µs
http_req_tls_handshaking.......: avg=24.54ms min=0s med=0s max=79.77ms p(90)=65.05ms p(95)=72.41ms
http_req_waiting...............: avg=89.78ms min=40.49ms med=65.94ms max=167.5ms p(90)=153.24ms p(95)=160.37ms
http_reqs......................: 5 0.485163/s
iteration_duration.............: avg=131.04ms min=41.02ms med=66.46ms max=305.21ms p(90)=262.59ms p(95)=283.9ms
iterations.....................: 4 0.388131/s
vus............................: 0 min=0 max=0
running (10.3s), 0/1 VUs, 4 complete and 0 interrupted iterations
constant_arrival_rate ✓ [======================================] 0/1 VUs 10s 0.33 iters/s
Misskeyでは、短すぎる間隔でコメントすると反映されない?っぽいので、3秒間隔で10秒間繰り返し実行するoption設定を入れました。
setup()は初回の一度のみ実行させることができるので、4 complete
となっており4回コメントしたことになります。
{
"api_duration": {
"values": {
"p(90)": 153.95769180000002,
"p(95)": 161.1491394,
"avg": 90.2026734,
"min": 40.713239,
"med": 66.177258,
"max": 168.340587
},
"type": "trend",
"contains": "time"
},
"checks": {
"type": "rate",
"contains": "default",
"values": {
"rate": 1,
"passes": 5,
"fails": 0
}
}
}
出力結果はこんな感じです。
この結果だけで分かりにくいのですが、ここで問題なのが集計結果にsetup()の結果も含まれていることです。k6の仕様としてそうなっているようです。
default function()の結果のみで集計したい場合は次のカスタムメトリクスを使うことで解決できます。
MisskeyのTLを確認すると、3秒間隔で4回分コメントされていることが分かります。
カスタムメトリクス(とグループ、タグ)
このようなスクリプトを用意しました。
import http from "k6/http";
import { check, group, sleep } from "k6";
import { Trend } from "k6/metrics";
import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.2/index.js";
const api_duration = new Trend("api_duration");
export function setup() {
const res = http.get("https://pekoraskey.melanmeg.com/");
check(res, { "is status 200": (r) => r.status === 200 });
}
export default function () {
group("vu", function () {
const res = http.get("https://pekoraskey.melanmeg.com/", {
tags: {
name: "test",
},
});
check(res, { "is status 200": (r) => r.status === 200 });
api_duration.add(res.timings.duration, { name: "test" });
});
sleep(1); // handleSummary()を使う場合、少し待たないとなぞに結果に反映されないため
}
export function teardown() {
const res = http.get("https://pekoraskey.melanmeg.com/");
check(res, { "is status 200": (r) => r.status === 200 });
}
export function handleSummary(data) {
// なぜか!で否定を使うと結果にでない
for (const key in data.metrics) {
if (key.startsWith("iteration")) delete data.metrics[key];
if (key.startsWith("http")) delete data.metrics[key];
if (key.startsWith("data")) delete data.metrics[key];
if (key.startsWith("group")) delete data.metrics[key];
if (key.startsWith("vus")) delete data.metrics[key];
}
const result = data.metrics;
const outputDir = "/home/k6/result.json";
return {
stdout: textSummary(data, { indent: " ", enableColors: true }),
[outputDir]: JSON.stringify(result),
};
}
実行結果
█ setup
✓ is status 200
█ vu
✓ is status 200
█ teardown
✓ is status 200
api_duration...: avg=95.598704 min=95.598704 med=95.598704 max=95.598704 p(90)=95.598704 p(95)=95.598704
checks.........: 100.00% ✓ 3 ✗ 0/
今更に使ったのですが、オプションで--quiet
つけるとk6のロゴ辺りの出力を消せます。
結果の通り、avg,minなどが全て同じ値でありdefault function()のテストだけで集計されるというわけです。ついでに標準出力もapi_duration
とchecks
だけにしてみました。
また、group化することで「█ vu」のようにイイ感じにできます。
tagの機能についてですが、この使い方は分かってません💦
オプションの--out csv=xxx
にて結果出力したときにtags列として出力されましたが、その他の場合出力されずよくわかりません。一応付けましたがなくてもいいかも。
k6 browserとは
k6 browserは、ブラウザベースのE2Eテストができるツールです。
k6と同様、並列にシナリオテストを実行することもできます。
以下のような特徴があります。
- Playwrightとおおよその互換性がある
- ブラウザ操作に基づいたパフォーマンステストが可能
- 操作に基づいて実行される各APIのテスト状況も確認できる
k6 browserもコンテナで
まずdockerfileのイメージをk6 browser兼用のイメージに書き変えます。
alpineに日本語フォントもインストールします。
日本語フォントがないと文字化けする
1c1
< FROM grafana/k6:0.51.0
---
> FROM grafana/k6:0.51.0-with-browser
3a4
> RUN apk add --no-cache fontconfig ttf-freefont font-noto-cjk
\ No newline at end of file
サンプルのテストシナリオを用意
ユーザーは事前に作成して、ユーザー名/パスワードを変更。
import { browser } from "k6/experimental/browser";
export const options = {
scenarios: {
ui: {
executor: "shared-iterations",
vus: 2,
iterations: 3,
options: {
browser: {
type: "chromium",
},
},
},
},
};
export default async function () {
const context = browser.newContext({
locale: "ja-JP",
});
const page = context.newPage();
try {
await page.goto("https://pekoraskey.melanmeg.com/");
page.waitForLoadState("load");
page.waitForSelector("button[data-cy-signin]");
page.click("button[data-cy-signin]");
page.locator('input[placeholder="ユーザー名"]').type("test");
page.locator('input[placeholder="パスワード"]').type("xxxxxxxx");
const submitButton = page.locator('button[type="submit"]');
await Promise.all([page.waitForNavigation(), submitButton.click()]);
page.waitForLoadState("load");
page.waitForSelector(
"#misskey_app > div > div.xFdHz > div:nth-child(2) > div:nth-child(1) > div:nth-child(2) > div > div > div > div > div > div > div > div.xzSZs.xwehs > div > div:nth-child(1) > article"
);
page.screenshot({ path: "/screenshot.png" });
} finally {
page.close();
context.close();
}
}
実行結果
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: test5.js
output: -
scenarios: (100.00%) 1 scenario, 2 max VUs, 10m30s max duration (incl. graceful stop):
* ui: 3 iterations shared among 2 VUs (maxDuration: 10m0s, gracefulStop: 30s)
INFO[0001] Misskey v2023.12.0 browser_source=console-api source=browser
INFO[0001] Misskey v2023.12.0 browser_source=console-api source=browser
WARN[0001] sid:80810AAA02E43DEB8D4294718C48960A tid:53F58DBAD25846EA2E0201DF8217750E bctxid:DB3A666BECE010AFFAC8407DF5A2A2BA bctx nil:false, unknown target type: "service_worker" category="Browser:isAttachedPageValid" elapsed="0 ms" source=browser
WARN[0001] sid:E35A5C2757BF9D51E0B4800D4D9C22A1 tid:9C3F59B84EE6F32CB5D87BE13D2FAAC1 bctxid:DAD71533CF995230E334A637986AA607 bctx nil:false, unknown target type: "service_worker" category="Browser:isAttachedPageValid" elapsed="0 ms" source=browser
ERRO[0002] Failed to load resource: the server responded with a status of 500 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/files/0712f571-06c2-4b12-a7b6-afdb051ed357"
ERRO[0003] Failed to load resource: the server responded with a status of 404 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/api/users/show"
ERRO[0003] Failed to load resource: the server responded with a status of 404 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/api/users/show"
ERRO[0003] Failed to load resource: the server responded with a status of 404 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/api/users/show"
ERRO[0003] Failed to load resource: the server responded with a status of 404 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/api/users/show"
ERRO[0003] Failed to load resource: the server responded with a status of 404 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/api/users/show"
ERRO[0003] Failed to load resource: the server responded with a status of 404 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/api/users/show"
ERRO[0005] Failed to load resource: the server responded with a status of 500 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/proxy/preview.webp?url=https%3A%2F%2Fi.ytimg.com%2Fvi%2FBCt3E-FJFdA%2Fmaxresdefault.jpg&preview=1"
ERRO[0007] Failed to load resource: the server responded with a status of 429 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/api/signin"
INFO[0007] Misskey v2023.12.0 browser_source=console-api source=browser
ERRO[0007] Failed to load resource: the server responded with a status of 500 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/files/0712f571-06c2-4b12-a7b6-afdb051ed357"
INFO[0010] Misskey v2023.12.0 browser_source=console-api source=browser
WARN[0010] sid:188F982CE9430FD3837132FB1678E19F tid:CDF06521029E4C8A5568A45D3F3180D8 bctxid:EE43FEA1F80361A53863322391B05C44 bctx nil:false, unknown target type: "service_worker" category="Browser:isAttachedPageValid" elapsed="0 ms" source=browser
ERRO[0011] Failed to load resource: the server responded with a status of 404 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/api/users/show"
ERRO[0012] Failed to load resource: the server responded with a status of 404 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/api/users/show"
ERRO[0012] Failed to load resource: the server responded with a status of 404 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/api/users/show"
ERRO[0012] Failed to load resource: the server responded with a status of 500 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/files/thumbnail-8c0fc227-22b9-4f5c-a412-256fb3fda31c"
INFO[0012] object is too large and will be parsed partially category=parseConsoleRemoteObjectPreview elapsed="2064 ms" source=browser
INFO[0012] Error occurred during decoding image {"Symbol(_vod)":"","alt":"IMG_0134.jpeg","sizes":"","src":"https://pekoraskey.melanmeg.com/files/thumbnail-8c0fc227-22b9-4f5c-a412-256fb3fda31c","srcset":""} {"code":"0","message":"The source image cannot be decoded.","name":"EncodingError"} browser_source=console-api source=browser
INFO[0016] Misskey v2023.12.0 browser_source=console-api source=browser
ERRO[0018] Failed to load resource: the server responded with a status of 500 () browser_source=network line_number=0 source=browser stacktrace="<nil>" url="https://pekoraskey.melanmeg.com/files/thumbnail-8c0fc227-22b9-4f5c-a412-256fb3fda31c"
INFO[0018] object is too large and will be parsed partially category=parseConsoleRemoteObjectPreview elapsed="5503 ms" source=browser
INFO[0018] Error occurred during decoding image {"Symbol(_vod)":"","alt":"IMG_0134.jpeg","sizes":"","src":"https://pekoraskey.melanmeg.com/files/thumbnail-8c0fc227-22b9-4f5c-a412-256fb3fda31c","srcset":""} {"code":"0","message":"The source image cannot be decoded.","name":"EncodingError"} browser_source=console-api source=browser
ERRO[0033] Uncaught (in promise) waiting for navigation: timed out after 30s executor=shared-iterations scenario=ui
browser_data_received.......: 13 MB 400 kB/s
browser_data_sent...........: 202 kB 6.0 kB/s
browser_http_req_duration...: avg=133.99ms min=75µs med=80.21ms max=3.8s p(90)=153.25ms p(95)=193.38ms
browser_http_req_failed.....: 2.77% ✓ 15 ✗ 526
browser_web_vital_cls.......: avg=0.033081 min=0 med=0.008874 max=0.14368 p(90)=0.089758 p(95)=0.116719
browser_web_vital_fcp.......: avg=594.67ms min=443ms med=593.5ms max=825.4ms p(90)=759.08ms p(95)=792.24ms
browser_web_vital_fid.......: avg=2.59ms min=599.99µs med=2.19ms max=5ms p(90)=4.43ms p(95)=4.71ms
browser_web_vital_inp.......: avg=885.33ms min=584ms med=768ms max=1.3s p(90)=1.19s p(95)=1.25s
browser_web_vital_lcp.......: avg=1.65s min=815.59ms med=2.05s max=2.41s p(90)=2.3s p(95)=2.35s
browser_web_vital_ttfb......: avg=181.42ms min=29.4ms med=245.9ms max=346.3ms p(90)=308.26ms p(95)=327.28ms
data_received...............: 0 B 0 B/s
data_sent...................: 0 B 0 B/s
iteration_duration..........: avg=17.01s min=8.71s med=9.04s max=33.28s p(90)=28.43s p(95)=30.85s
iterations..................: 3 0.089439/s
vus.........................: 1 min=1 max=2
vus_max.....................: 2 min=2 max=2
running (00m33.5s), 0/2 VUs, 3 complete and 0 interrupted iterations
ui ✓ [======================================] 2 VUs 00m33.5s/10m0s 3/3 shared iters
並列数2, 反復数3で実行。
500, 404 エラーが入るのは元からです。
browser.newContextにパラメータを渡すことでブラウザの設定をすることができ、この場合、ロケールを日本にする設定を入れてます(デフォルトがen-USぽいので)
パラメータ一覧のドキュメントはここにあります↓
screenshot.pngを確認すると、ちゃんとログイン後の画面となっています。
例えばプロキシ環境でのテストがしたい場合に、このbrowser.newContextにhttpCredentialsの設定を入れる必要がありました。これを見つけるのはちょっと大変でした...🤦
おまけ
k6 + Typescript + opneapi-generatorのすすめ
APIテストシナリオを実装する上で、リクエストのスキーマに気を付けることがあると思います。多分...。型を自動生成して利用したおかげで、安全性が高く開発もスムーズに進められました。型はk6との相性がいいと思います。ぜひk6導入する際はこの構成を検討してみてほしいです。
その他やったこととか
案件ではこの他、k6のラッパーをスーパーエンジニアの方がRustで実装し、CI組み込みもされました🙄
cloudbuildの設定から、Teams通知の設定までパパっとできるように自分もなりたいなぁ
運用後に感じたこと
CIで運用が始まってから特に感じたものとして
- APIに変更があればすぐに気づける
- どのAPIで失敗したかわかる
- エラーログが見れる
- 前バージョンの結果と比較してデグレが分かる
やっぱ便利です。
総評
記事を書いて、改めてk6, k6 browserの可能性を感じました。特にk6 browser
パフォーマンステストツールの中でもデファクトだろうし、性能のテストに使うので性能面で良いGo言語で実装されている点もポイントが高いです。
k6 browserを使いたいとなる場面としては、k6での実装が困難なテストに関してはk6 browserを利用する使い方がいいのではというとこがありました。
またk6オプションは豊富で--traces-output string
や--profiling-enabled
オプションだったり、グループ化機能でのtagWithCurrentStageProfile()
などこの辺りも調査してみたいです。
今回取り上げていませんが、k6を使いこなすにはk6/timersなど各種モジュールやテストを中断させるにはちょっと気づきくいtest.abort()
を使ったりすると良いかもです。
またAPIドキュメントを読む際の注意点として、https://k6.io/docs/javascript-api/
サイトには最新バージョンが載っていないので https://grafana.com/docs/k6/latest/
こっちを見ないといけないよう。
比較対象があるでもないですが、ざっと評価してみました。
APIテスト | E2Eテスト | CI組み込み | ツールのパフォーマンス | 並列実行 |
---|---|---|---|---|
〇 | △ | 〇 | ◎ | ◎ |
- E2Eテストは実験的モジュールであること、ドキュメントや参考資料が少ないこと、またplaywrightやchromiumの知見もないと実装が難しそうなので△くらいに
- APIテストは、k6モジュールとnodejs標準モジュールとの相性でスムーズに実装できないときがあったので〇(スキル不足だった可能性が高い💦)
- CI組み込みは、k6 browserのプロキシ設定に手こずったので〇にした
個人的にplaywrightが好きなので、それを並列実行する仕組みは興味深いと思いました。
こうやって記事にまとめると知識定着になりますね👊
k6面白い!
以上
Discussion