負荷テストをk6+TypeScriptで構成する

## はじめに
k6とTypeScriptを利用した負荷テストコードについて紹介します。
本テストコードは、以下のリポジトリを参考にしています。
https://github.com/tom-miseur/k6-templates

## ディレクトリ構成
ページを組み合わせて様々なシナリオを組み立てられるように、シナリオとページを分け、拡張しやすい構成としています。
```
.
└── dist
│    └── main.js                  # 実行ファイル
└── src
　    ├── @types
　    │   ├── global.d.ts         # グローバル定義
　    └── config
　    │   ├── executors.json      # 実行オプション定義ファイル
　    │   └── thresholds.json     # 閾値オプション定義ファイル
　    └── scenarios
　    │   ├── scenario.ts         # シナリオ抽象クラス
　    │   └── scenario1.ts        # シナリオクラス
　    │   └── scenario2.ts
　    │   └── scenario_loader.ts　# シナリオクラスローダー
　    └── scripts                  # ページスクリプト
　    │   ├── page1.ts
　    │   └── page2.ts
　    │   └── page3.ts
　    └── main.ts                  # エントリースクリプト

```

## テストコード

下記のように、環境変数でシナリオを指定し、実行する構成としています。

```
k6 run -e SCENARIO=scenario1 -e BASE_URL=https://hoge.jp dist/main.js
※BASE_URLは任意
```

### エントリースクリプト

シナリオに対応する定義ファイルを読み込み、シナリオを実行します。

```src/main.ts
import { Options } from 'k6/options'
import { Scenario } from '@/src/scenarios/scenario'
import { load } from '@/src/scenarios/scenario_loader'

globalThis.BASE_URL = __ENV.BASE_URL || 'https://fuga.jp'

if (!__ENV.SCENARIO) {
  throw new Error('__ENV.SCENARIO not specified.')
}

// シナリオクラスの読み込み
const scenario: Scenario = load(__ENV.SCENARIO)
// 定義ファイルの読み込み
const executors = JSON.parse(open('../src/config/executors.json'))
const thresholds = JSON.parse(open('../src/config/thresholds.json'))
// 読み込んだ定義ファイルの内容をオプションに設定
export const options: Options = {
  scenarios: { scenarioName: executors[__ENV.SCENARIO] },
  thresholds: thresholds[__ENV.SCENARIO] ? thresholds[__ENV.SCENARIO] : {}
}

// Test lifecycle function ※中身はシナリオクラスで実装します

export function setup(): any {
  return scenario.setup()
}

export default function (data: any) {
  scenario.execute(data)
}

export function teardown(data: any) {
  scenario.teardown(data)
}
```

```src/@types/global.d.ts
export {}

declare global {
  var BASE_URL: string
}
```

### 定義ファイル

#### 実行オプション定義ファイル

実行オプション定義ファイルでは、シナリオごとにVUや実行時間など、テスト実行に必要なオプションを定義します。executorによって、実行オプションの指定が異なりますので、詳細は[こちら](https://k6.io/docs/using-k6/scenarios/executors/)を参照してください。
また、テストの目的に応じたオプションの指定については、[こちら](https://k6.io/docs/test-types/introduction/)が参考になります。

```src/config/executors.json
{
  "scenario1": {
    "executor": "ramping-vus",
    "startVUs": 5,
    "stages": [
      {"duration": "10s", "target": 15},
      {"duration": "10s", "target": 20},
      {"duration": "10s", "target": 15},
      {"duration": "10s", "target": 5}
    ]
  },
  "scenario2": {
    "executor": "shared-iterations",
    "iterations": 40,
    "vus": 20
  }
}
```

#### 閾値オプション定義ファイル

閾値オプション定義ファイルでは、シナリオごとに合否基準を定義します。
以下の例ではシナリオでリクエストされるページに対して、閾値を定義しています。

```src/config/thresholds.json
{
  "scenario1": {
    "group_duration{group:::page1}": ["avg<400"],
    "group_duration{group:::page2}": ["avg<400", "p(90)<800"],
    "group_duration{group:::page3}": ["avg<400"]
  }
}
```

### シナリオクラス

テストライフサイクルをシナリオクラス側で実装するためと、シナリオによってはsetupやteardownが不要なケースがあるため、シナリオの抽象クラスを置き、それを実装する構成にしています。

```（シナリオ抽象クラス）src/scenarios/scenario.ts
export abstract class Scenario
{
  setup(): any {}
  abstract execute(data: any): void
  teardown(data: any): void {}
}
```

シナリオクラスは、復数のページをまとめ、シナリオとして管理します。以下の例ではpage1とpage2を読み込んでいます。
前処理や後処理が必要な場合は、setupやteardownメソッドをオーバーライドします。

```（シナリオクラス）src/scenarios/scenario1.ts
import { Scenario } from '@/src/scenarios/scenario'
import { page1 } from '@/src/scripts/page1'
import { page2 } from '@/src/scripts/page2'

export class Scenario1 extends Scenario
{
  execute(data: any): void {
    page1()
    page2()
  }
}
```

シナリオクラスローダーは、環境変数で指定されたシナリオ名から対応するシナリオクラスを返すためのものです。（予め用意したシナリオ名とシナリオクラスのマッピングから、対象のクラスを返す単純なものです）

```（シナリオクラスローダー）src/scenarios/scenario_loader.ts
import { Scenario } from '@/src/scenarios/scenario'
import { Scenario1 } from '@/src/scenarios/scenario1'
import { Scenario2 } from '@/src/scenarios/scenario2'

// シナリオ名とシナリオクラスのマッピング
const classes: {[name: string]: Scenario} = {
  'scenario1': new Scenario1(),
  'scenario2': new Scenario2(),
}

export function load(scenarioName: string): Scenario {
  if (!classes.hasOwnProperty(scenarioName)) {
    throw new Error(`scenario class not found.(${scenarioName})`)
  }
  return classes[scenarioName]
}
```

### ページスクリプト

リクエストの発行を管理するスクリプトになります。ランダムなリクエストパラメーターの生成など、ページごとのリクエスト管理はこちらで実装する形になります。

```src/scripts/page1.ts
import { group, sleep } from 'k6'
import http from 'k6/http'

export function page1() {
  group('page1', function () {
    http.get(`${globalThis.BASE_URL}/page1`)
  })
  sleep(1)
}
```

以上でコードの説明は終わりです。
本記事に記載しておりませんが、コンパイルオプション等につきましては、[こちらのテンプレート](https://github.com/grafana/k6-template-typescript)が参考になるかと思います。