-
Notifications
You must be signed in to change notification settings - Fork 0
[Chore/performance test] - 성능 테스트 스크립트 추가 #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c284bf6
6ad9ba3
4b687ca
42670d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # Dependencies | ||
| node_modules/ | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| package-lock.json | ||
|
|
||
| # Build outputs | ||
| dist/ | ||
| build/ | ||
| *.tsbuildinfo | ||
|
|
||
| # Environment files | ||
| .env | ||
| .env.* | ||
| !.env.example | ||
|
|
||
| # Test reports and outputs | ||
| reports/ | ||
| summary.html | ||
| summary.json | ||
| summary.txt | ||
| *.log | ||
|
|
||
| # k6 specific | ||
| k6-*-report.html | ||
| k6-results.json | ||
|
|
||
| # IDE files | ||
| .vscode/ | ||
| .idea/ | ||
| *.swp | ||
| *.swo | ||
| *~ | ||
|
|
||
| # OS generated files | ||
| .DS_Store | ||
| .DS_Store? | ||
| ._* | ||
| .Spotlight-V100 | ||
| .Trashes | ||
| ehthumbs.db | ||
| Thumbs.db | ||
|
|
||
| # Temporary files | ||
| *.tmp | ||
| *.temp | ||
|
|
||
| # Coverage reports | ||
| coverage/ | ||
| .nyc_output/ | ||
|
|
||
| # Dependency directories | ||
| jspm_packages/ | ||
|
|
||
| # Optional npm cache directory | ||
| .npm | ||
|
|
||
| # ESLint cache | ||
| .eslintcache | ||
|
|
||
| # Editor directories and files | ||
| .vscode/* | ||
| !.vscode/extensions.json | ||
| .idea | ||
| *.suo | ||
| *.ntvs* | ||
| *.njsproj | ||
| *.sln | ||
| *.sw? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| import http from 'k6/http'; | ||
| import { check, sleep } from 'k6'; | ||
| import { SharedArray } from 'k6/data'; | ||
| import { Trend } from 'k6/metrics'; | ||
|
|
||
| const BASE_URL = 'http://localhost:8080/api/v1'; | ||
| const USERS_ID = new SharedArray('users', function () { | ||
| const arr = []; | ||
| for(let i = 1; i< 51; i++) { | ||
| arr.push(i); | ||
| } | ||
| return arr; | ||
| }); | ||
|
|
||
| let tokens = []; | ||
|
|
||
| const requestCount = new Trend('request_count', true); | ||
|
|
||
| export const options = { | ||
| thresholds: { | ||
| http_req_failed: ['rate<0.01'], | ||
| http_req_duration: ['p(95)<1000'], | ||
| }, | ||
|
|
||
| scenarios: { | ||
| normal: { | ||
| executor: 'per-vu-iterations', | ||
| vus: 50, // 50명의 가상 사용자 | ||
| iterations: 400, // 각 가상 사용자가 400번의 요청을 수행 | ||
| maxDuration: '2m', // 테스트 전체 지속 시간 | ||
| exec: 'normal' // 기록 범위 조회 API | ||
| }, | ||
|
|
||
| mypage: { | ||
| executor: 'constant-arrival-rate', | ||
| rate: 10, | ||
| timeUnit: '4s', // 4초 당 10회 요청 | ||
| duration: '2m', // 2분 지속 | ||
| preAllocatedVUs: 4, | ||
| maxVUs: 50, // 최대 50명의 가상 사용자 | ||
| exec: 'mypage', // 마이페이지 조회 API | ||
| }, | ||
| }; | ||
|
|
||
| export function setup() { | ||
| console.log('테스트 셋업 시작: 사용자 로그인 및 토큰 획득'); | ||
|
|
||
| for (const user of USERS_ID) { | ||
| const res = http.post(`${BASE_URL}/auth/test/login`, JSON.stringify({ user_id: user }), { | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
|
|
||
| const success = check(res, { | ||
| 'logged in successfully': (r) => r.status === 200 && r.json('payload.tokens.access_token'), | ||
| }); | ||
|
|
||
| if (success) { | ||
| tokens.push({ | ||
| user_id: user, | ||
| accessToken: res.json('payload.tokens.access_token'), | ||
| refreshToken: res.json('payload.tokens.refresh_token'), | ||
| }); | ||
| } else { | ||
| console.error(`사용자 ${user} 로그인 실패: ${res.status}`); | ||
| } | ||
| } | ||
|
|
||
| console.log(`셋업 완료: ${tokens.length}개의 토큰 획득`); | ||
| return { tokens }; | ||
| } | ||
|
|
||
| export default function (data) { | ||
|
|
||
| } | ||
|
|
||
|
|
||
|
|
||
| export function mypage (data) { | ||
| const vuIndex = (__VU - 1) % data.tokens.length; | ||
| let { user_id, accessToken } = data.tokens[vuIndex]; | ||
| let res = http.get(`${BASE_URL}/users/me`, { | ||
| headers: { Authorization: `Bearer ${accessToken}` }, | ||
| }); | ||
| if (res.status === 401) { | ||
| const newToken = refreshAccessToken(data.tokens[vuIndex].refreshToken); | ||
| if (newToken) { | ||
| accessToken = newToken; | ||
| res = http.get(`${BASE_URL}/records/me${queryString}`, { | ||
| headers: { Authorization: `Bearer ${newToken}` }, | ||
| }); | ||
| data.tokens[vuIndex].accessToken = newToken; | ||
| requestCount.add(1); | ||
| } | ||
| } | ||
| check(res, { | ||
| 'status is 200': (r) => r.status === 200, | ||
| }); | ||
| } | ||
ekgns33 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
|
|
||
|
|
||
| export function normal(data ) { | ||
| const vuIndex = (__VU - 1) % data.tokens.length; | ||
| let { user_id, accessToken } = data.tokens[vuIndex]; | ||
| const page = randomIntBetween(0, 5); | ||
| const size = randomIntBetween(1, 20); | ||
| let startDate = getRandomDateInPast(30); | ||
| let endDate = getRandomDateInPast(30); | ||
|
|
||
| if (startDate > endDate) { | ||
| const tmp = startDate; | ||
| startDate = endDate; | ||
| endDate = tmp; | ||
| } | ||
|
|
||
| const queryString = `?page=${page}&size=${size}&startDate=${formatDate(startDate)}&endDate=${formatDate(endDate)}`; | ||
|
|
||
| let res = http.get(`${BASE_URL}/records/me${queryString}`, { | ||
| headers: { Authorization: `Bearer ${accessToken}` }, | ||
| }); | ||
|
|
||
|
|
||
| requestCount.add(1); | ||
|
|
||
| if (res.status === 401) { | ||
| const newToken = refreshAccessToken(data.tokens[vuIndex].refreshToken); | ||
| if (newToken) { | ||
| accessToken = newToken; | ||
| res = http.get(`${BASE_URL}/records/me${queryString}`, { | ||
| headers: { Authorization: `Bearer ${newToken}` }, | ||
| }); | ||
| data.tokens[vuIndex].accessToken = newToken; | ||
| requestCount.add(1); | ||
| } | ||
| } | ||
| check(res, { | ||
| 'status is 200': (r) => r.status === 200, | ||
| }); | ||
| } | ||
|
|
||
| function runStressScenario(user_id, access_token, vuIndex) { | ||
|
|
||
| let res = http.get(`${BASE_URL}/users/me`, { | ||
| headers: { Authorization: `Bearer ${accessToken}` }, | ||
| }); | ||
| if (res.status === 401) { | ||
| const newToken = refreshAccessToken(data.tokens[vuIndex].refreshToken); | ||
| if (newToken) { | ||
| accessToken = newToken; | ||
| res = http.get(`${BASE_URL}/records/me${queryString}`, { | ||
| headers: { Authorization: `Bearer ${newToken}` }, | ||
| }); | ||
| data.tokens[vuIndex].accessToken = newToken; | ||
| requestCount.add(1); | ||
| } | ||
| } | ||
| check(res, { | ||
| 'status is 200': (r) => r.status === 200, | ||
| }); | ||
| } | ||
ekgns33 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export function teardown() { | ||
| const totalDurationInSeconds = 10 + 60 + 30; | ||
| const totalRequests = requestCount._sum; | ||
ekgns33 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const throughput = totalRequests / totalDurationInSeconds; | ||
| console.log(`\n---\n Throughput (requests/sec): ${throughput.toFixed(2)}\n---`); | ||
| } | ||
|
|
||
| // Utils | ||
| function randomIntBetween(min, max) { | ||
| return Math.floor(Math.random() * (max - min + 1)) + min; | ||
| } | ||
|
|
||
| function formatDate(date) { | ||
| return date.toISOString().slice(0, 10); | ||
| } | ||
|
|
||
| function getRandomDateInPast(daysAgo = 30) { | ||
| const date = new Date(); | ||
| date.setDate(date.getDate() - randomIntBetween(0, daysAgo)); | ||
| return date; | ||
| } | ||
|
|
||
| function refreshAccessToken(refreshToken) { | ||
| const res = http.post(`${BASE_URL}/auth/refresh`, JSON.stringify({ | ||
| refresh_token: refreshToken | ||
| }), { | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${refreshToken}` | ||
| }, | ||
| }); | ||
| if (res.status === 200 && res.json('payload.access_token')) { | ||
| return res.json('payload.access_token'); | ||
ekgns33 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| return null; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| ## 🚀 설치 및 실행 | ||
|
|
||
| ### 1. 의존성 설치 | ||
| ```bash | ||
| cd performance | ||
| npm install | ||
| ``` | ||
|
|
||
| ### 2. 환경 설정 | ||
| ```bash | ||
| cp .env.example .env | ||
| # .env 파일을 편집하여 실제 API URL 및 설정값 입력 | ||
| ``` | ||
ekgns33 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ### 3. 빌드 | ||
| ```bash | ||
| npm run build | ||
| ``` | ||
|
|
||
| ### 4. 테스트 실행 | ||
|
|
||
| #### 개별 테스트 실행 | ||
| ```bash | ||
| # 기본 부하 테스트 | ||
| npm run test:basic-load | ||
|
|
||
| # 간단한 반복 테스트 | ||
| npm run test:simple | ||
|
|
||
| # 실제 사용자 시뮬레이션 (22분 소요) | ||
| npm run test:real-user | ||
|
|
||
| # 다중 시나리오 테스트 | ||
| npm run test:multi | ||
| ``` | ||
|
|
||
| #### 환경변수와 함께 실행 | ||
| ```bash | ||
| API_BASE_URL=https://your-api.com/v1 MAX_USERS=100 k6 run dist/basic-load.js | ||
| ``` | ||
|
|
||
| #### 모든 테스트 실행 | ||
| ```bash | ||
| npm run test:all | ||
| ``` | ||
|
|
||
| ### 개발 모드 (파일 변경 감지) | ||
| ```bash | ||
| npm run build:watch | ||
| ``` | ||
|
|
||
| ### 새로운 테스트 추가 | ||
| 1. `src/tests/` 디렉토리에 새 TypeScript 파일 생성 | ||
| 2. `webpack.config.js`의 entry에 새 파일 추가 | ||
| 3. `package.json`의 scripts에 실행 명령 추가 | ||
|
|
||
| ### 새로운 시나리오 추가 | ||
| 1. `src/scenarios/` 디렉토리에 새 시나리오 클래스 생성 | ||
| 2. 기존 테스트에서 import하여 사용 | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,25 @@ | ||||||||||
| { | ||||||||||
| "name": "k6-performance-tests", | ||||||||||
| "version": "1.0.0", | ||||||||||
| "description": "K6 performance tests written in TypeScript", | ||||||||||
| "scripts": { | ||||||||||
| "build": "webpack", | ||||||||||
| "build:watch": "webpack --watch", | ||||||||||
| "clean": "rm -rf dist", | ||||||||||
| "test:basic-load": "k6 run dist/basic-load.js", | ||||||||||
| "test:simple": "k6 run dist/simple-iteration.js", | ||||||||||
| "test:real-user": "k6 run dist/real-user-simulation.js", | ||||||||||
| "test:multi": "k6 run dist/multi-scenario.js", | ||||||||||
| "test:all": "npm run build && npm run test:basic-load && npm run test:simple && npm run test:real-user && npm run test:multi" | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add missing test script for 200vu-simulation.js. The "test:multi": "k6 run dist/multi-scenario.js",
+ "test:200vu": "k6 run 200vu-simulation.js",
- "test:all": "npm run build && npm run test:basic-load && npm run test:simple && npm run test:real-user && npm run test:multi"
+ "test:all": "npm run build && npm run test:basic-load && npm run test:simple && npm run test:real-user && npm run test:multi && npm run test:200vu"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저건 js로 기존 레거시를 그냥 둔거라서 참고용이야 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ekgns33 Thanks for the clarification! I understand now that the ✏️ Learnings added
|
||||||||||
| }, | ||||||||||
| "devDependencies": { | ||||||||||
| "@types/k6": "^0.47.0", | ||||||||||
| "typescript": "^5.0.0", | ||||||||||
| "webpack": "^5.88.0", | ||||||||||
| "webpack-cli": "^5.1.0", | ||||||||||
| "ts-loader": "^9.4.0" | ||||||||||
| }, | ||||||||||
|
Comment on lines
+15
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainVerify dependency versions for security and compatibility. The dependency versions should be checked for security vulnerabilities and compatibility with the latest k6 features. 🌐 Web query: 💡 Result: As of June 30, 2025, the latest stable versions of the specified packages are:
Regarding security vulnerabilities, as of the latest available information:
Please note that security landscapes can change over time. It's advisable to regularly check official sources or security advisories for the most current information. Citations: Dependencies mostly up to date; confirm unstable versions No known security vulnerabilities were found in the specified versions, but a couple of entries merit your attention in performance/package.json (lines 15–21):
Please adjust or confirm these versions to ensure compatibility. 🤖 Prompt for AI Agents |
||||||||||
| "keywords": ["k6", "performance", "testing", "typescript"], | ||||||||||
| "author": "", | ||||||||||
| "license": "MIT" | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| export const config = { | ||
| baseUrl: __ENV.API_BASE_URL || 'http://localhost:8080/api/v1', | ||
| maxUsers: parseInt(__ENV.MAX_USERS || '1'), | ||
| testDuration: __ENV.TEST_DURATION || '2m', | ||
|
|
||
| // Thresholds | ||
| httpReqDurationP95: parseInt(__ENV.HTTP_REQ_DURATION_P95 || '1000'), | ||
| httpReqFailedRate: parseFloat(__ENV.HTTP_REQ_FAILED_RATE || '0.01'), | ||
|
|
||
| // Report settings | ||
| enableHtmlReport: (__ENV.ENABLE_HTML_REPORT || 'true') === 'true', | ||
| }; | ||
|
|
||
| export const thresholds = { | ||
| http_req_failed: [`rate<${config.httpReqFailedRate}`], | ||
| http_req_duration: [`p(95)<${config.httpReqDurationP95}`], | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.