技術(tech)

k6 – シナリオをテストするためにモックAPIサーバを構築する

はじめに

k6で負荷試験用のスクリプトを作成する際に、負荷シナリオに対するテストをどう書けばいいものなのか、疑問が湧きます。

テストの方法として、単体テスト、結合テスト等、いくつかの方法があると思います。

その中でも、今回はモックAPIサーバを準備して、負荷シナリオに対するテストを行う方法を紹介します。

先に結論から

イメージ図は以下の通りです。

Mock API ServerをExpressを使って立てます。

各シナリオからは、実際のWebサーバではなく、Mock API Serverへアクセスします。
Mock API Serverはどんなエンドポイントが叩かれても、200 okを返却するAPIを用意します。
個別にMock APIのレスポンスを制御したい場合には、その都度APIを準備します。

なお、この構成で出来ること、出来ないことは以下の通りです。

  • 出来ること
    • k6のシナリオの動作の担保
  • 出来ないこと
    • k6内で定義した関数やモジュールの振る舞いの担保
    • リクエスト先のAPIのレスポンスが正しいことの担保

シナリオを動かしても、エラーが起きないよね?程度の確認となります。

実際のMock API Serverの作成は、以下のPRが参考になります。
https://github.com/gonkunkun/k6-template/pull/2

Mock API Serverを立ててみよう

それでは実際にサーバを立てていきましょう。

ディレクトリ構成は以下をご参考にどうぞ。
https://github.com/gonkunkun/k6-template/

~/D/g/t/mock ❯❯❯ tree ./ -I node_modules
./
├── package-lock.json
├── package.json
├── src
│   ├── index.ts
│   └── sampleProduct
│       └── routes.ts
└── tsconfig.json

Mock API Serverを立てる

以下のようなpackage.jsonファイルを準備します。

{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.ts",
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/express": "^4.17.21",
    "express": "^4.19.2",
    "ts-node": "^10.9.2",
    "typescript": "^5.4.3"
  }
}

そしたら、モジュール郡をinstallします。

~/D/g/template-of-k6 ❯❯❯ cd mock 
~/D/g/t/mock ❯❯❯ npm install 

srcディレクトリ配下にサーバの設定を追加します。
以下、src/index.tsを作成します。

import express, { Request, Response } from 'express'
import sampleProductRouter from './sampleProduct/routes'

const app = express()
const port = process.argv || 3003

app.get('/', (req: Request, res: Response) => {
  console.log(req.body)
  res.status(200).json({ message: 'This is mock endpoint' })
})

app.use('/sampleProduct', sampleProductRouter)

app.listen(port, () => console.log(`Listening on port ${port}`))

これで、ルートの/* に対するレスポンスを定義出来ました。

更に、将来的に複数のプロダクトがこのk6スクリプトを利用することを考慮して、プロダクト毎にエンドポイントを分けましょう。

import { Router, Request, Response } from 'express'

const router = Router()

router.get('/sample', (req: Request, res: Response) => {
  console.log(req.body)
  res.status(200).json({ message: 'This is sampleProduct endpoint' })
})

router.use((req: Request, res: Response) => {
  console.log(req.body)
  res.status(200).json({ message: 'OK' })
})

export default router

これで、ルートの/sampleProduct に対するレスポンスを定義出来ました。

上記のAPIはひたすら200 okのみを返却します。
例えば、/sample/testに対するレスポンスを更に細かく制御したいという場合には、都度都度新しいAPIを追加してあげましょう。

Mock API Serverが完成したら、サーバを起動します。

~/D/g/t/mock ❯❯❯ npx ts-node ./src/index 3005
Listening on port 3005

試しにAPIを叩いてみます。

~/D/g/template-of-k6 ❯❯❯ curl -XGET localhost:3005/sampleProduct
{"message":"OK"}%                                                                                                  
~/D/g/template-of-k6 ❯❯❯ curl -XGET localhost:3005/sampleProduct/test
{"message":"OK"}% 

良さそうですね。

k6からMock API Serverを利用する

後は、k6実行時のエンドポイントをMockに向けるだけです。

サンプルアプリの中では.envで環境変数を渡していますので、ここのエンドポイントを例えばSAMPLE_PRODUCT_ENDPOINT=http://localhost:3005に修正してあげます。
https://github.com/gonkunkun/k6-template/blob/main/.env.sample#L4

あとはk6を実行するだけです。

~/D/g/template-of-k6 ❯❯❯ npm run smoke:sample-product                                                            ✘ 255 main ✱

          /\      |‾‾| /‾‾/   /‾‾/   
     /\  /  \     |  |/  /   /  /    
    /  \/    \    |     (   /   ‾‾\  
   /          \   |  |\  \ |  (‾)  | 
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: ./dist/loadTest.js
     output: -

  scenarios: (100.00%) 2 scenarios, 2 max VUs, 1m30s max duration (incl. graceful stop):
           * sampleScenario1: 1 iterations for each of 1 VUs (maxDuration: 1m0s, exec: sampleScenario1, gracefulStop: 30s)
           * sampleScenario2: 1 iterations for each of 1 VUs (maxDuration: 1m0s, exec: sampleScenario2, gracefulStop: 30s)

INFO[0003] 0se: == setup() BEGIN ===========================================================  source=console
INFO[0003] 0se: Start of test: 2024-03-31 19:11:07       source=console
INFO[0003] 0se: Test environment: local                  source=console
INFO[0003] 0se: == Check scenario configurations ======================================================  source=console
INFO[0003] 0se: Scenario: sampleScenario1()              source=console
INFO[0003] 0se: Scenario: sampleScenario2()              source=console
INFO[0003] 0se: == Check scenario configurations FINISHED ===============================================  source=console
INFO[0003] 0se: == Initialize Redis ======================================================  source=console
INFO[0003] 0se: == setup() END ===========================================================  source=console
INFO[0009] 6se: Scenario sampleScenario1 is initialized. Lens is 10000  source=console
INFO[0009] 6se: Scenario sampleScenario2 is initialized. Lens is 10000  source=console
INFO[0009] 6se: == Initialize Redis FNISHED ===============================================  source=console
INFO[0009] 6se: sampleScenario2() start ID: 2, vu iterations: 1, total iterations: 0  source=console
INFO[0009] 6se: sampleScenario1() start ID: 2, vu iterations: 1, total iterations: 0  source=console
INFO[0010] 7se: sampleScenario2() end ID: 2, vu iterations: 1, total iterations: 0  source=console
INFO[0010] 7se: sampleScenario1() end ID: 2, vu iterations: 1, total iterations: 0  source=console
INFO[0010] 0se: == All scenarios FINISHED ===========================================================  source=console
INFO[0010] 0se: == Teardown() STARTED ===========================================================  source=console
INFO[0010] 0se: == Initialize Redis ======================================================  source=console
INFO[0010] 0se: == Teardown() FINISHED ===========================================================  source=console
INFO[0010] 0se: == Initialize Redis FINISHED ===============================================  source=console

     █ setup

     █ sampleScenario2

       ✓ Status is 200

     █ sampleScenario1

       ✓ Status is 200

     █ teardown

     checks.........................: 100.00% ✓ 2        ✗ 0  
     data_received..................: 152 kB  21 kB/s
     data_sent......................: 939 kB  132 kB/s
     group_duration.................: avg=621.43ms min=621.42ms med=621.43ms max=621.45ms p(90)=621.45ms p(95)=621.45ms
     http_req_blocked...............: avg=395.64ms min=392.1ms  med=395.64ms max=399.17ms p(90)=398.46ms p(95)=398.81ms
     http_req_connecting............: avg=175.13ms min=175.12ms med=175.13ms max=175.14ms p(90)=175.14ms p(95)=175.14ms
     http_req_duration..............: avg=213.6ms  min=209.97ms med=213.6ms  max=217.23ms p(90)=216.5ms  p(95)=216.87ms
       { expected_response:true }...: avg=213.6ms  min=209.97ms med=213.6ms  max=217.23ms p(90)=216.5ms  p(95)=216.87ms
     http_req_failed................: 0.00%   ✓ 0        ✗ 2  
     http_req_receiving.............: avg=390.5µs  min=58µs     med=390.49µs max=723µs    p(90)=656.5µs  p(95)=689.75µs
     http_req_sending...............: avg=20.21ms  min=15.2ms   med=20.21ms  max=25.21ms  p(90)=24.21ms  p(95)=24.71ms 
     http_req_tls_handshaking.......: avg=187.4ms  min=183.88ms med=187.4ms  max=190.93ms p(90)=190.23ms p(95)=190.58ms
     http_req_waiting...............: avg=193ms    min=191.96ms med=193ms    max=194.04ms p(90)=193.83ms p(95)=193.93ms
     http_reqs......................: 2       0.280001/s
     iteration_duration.............: avg=1.93s    min=507.73µs med=625.79ms max=6.49s    p(90)=4.73s    p(95)=5.61s   
     iterations.....................: 2       0.280001/s
     vus............................: 2       min=0      max=2
     vus_max........................: 2       min=2      max=2

running (0m07.1s), 0/2 VUs, 2 complete and 0 interrupted iterations
sampleScenario1 ✓ [======================================] 1 VUs  0m00.6s/1m0s  1/1 iters, 1 per VU
sampleScenario2 ✓ [======================================] 1 VUs  0m00.6s/1m0s  1/1 iters, 1 per VU

これで、mock API Serverを利用して、シナリオを動作確認が出来ます。
わざわざ実サーバを立てなくても良い点が便利です。

おわりに

今回は、Mock API Serverを立てて、シナリオの動作の担保が出来るようにしてみました。

ここまで出来るようになると、次はこのテストをCIに組み込むことが出来ます。

以下の記事でその説明をしています。
興味があればご覧くださいませ。

k6 - CIでスモークテストを実行してシナリオの品質を担保するはじめに CIで継続的にテストを実施することは、プロダクト開発において非常に重要です。 負荷試験においても上記は例外ではなく、久しぶりに...