roomescape-refactored/test-scripts/create-schedule-scripts.js
pricelees 79de5c9c63 [#64] 결제 & 예약 확정 API에서의 트랜잭션 범위 수정 (#65)
<!-- 제목 양식 -->
<!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) -->

## 📝 관련 이슈 및 PR

**PR과 관련된 이슈 번호**
- #64

##  작업 내용
<!-- 어떤 작업을 했는지 알려주세요! -->
- 기존의 결제 시도 이력 테이블 조회 & 검증 -> 예약 / 일정 조회 및 검증을 하나의 트랜잭션으로 통합
- 예약 / 일정 LOCK 조회를 가장 먼저 수행 -> 배치와의 충돌을 방지하기 위함

## 🧪 테스트
<!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! -->
- 동일 조건에서 테스트했을 때 P95 응답 시간 749 -> 327ms로 50% 가량 개선 확인
- 커넥션 대기로 길어진 최대 API 응답 시간 7.70 -> 2.88초로 대폭 감소

## 📚 참고 자료 및 기타
<!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! -->

Reviewed-on: #65
Co-authored-by: pricelees <priceelees@gmail.com>
Co-committed-by: pricelees <priceelees@gmail.com>
2025-10-15 02:24:18 +00:00

136 lines
4.3 KiB
JavaScript

import http from 'k6/http';
import {check, sleep} from 'k6';
import exec from 'k6/execution';
import {BASE_URL, getHeaders, login, parseIdToString} from "./common.js";
import {randomIntBetween} from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
const TOTAL_ITERATIONS = 85212;
export const options = {
scenarios: {
schedule_creation: {
executor: 'shared-iterations',
vus: 263,
iterations: TOTAL_ITERATIONS,
maxDuration: '30m',
},
},
thresholds: {
'http_req_duration': ['p(95)<3000'],
'http_req_failed': ['rate<0.1'],
},
};
export function setup() {
console.log('=== Setup: 테스트 데이터 및 작업 목록 준비 중 ===');
const themesRes = http.get(`${BASE_URL}/tests/themes`);
const themes = parseIdToString(themesRes).results
const accountsRes = http.get(`${BASE_URL}/tests/admin/accounts`);
const accounts = JSON.parse(accountsRes.body).results;
const dates = generateDates(7);
console.log(`총 매장 수: ${accounts.length}`);
console.log(`총 테마 수: ${themes.length}`);
console.log(`생성 기간: ${dates.length}`);
const tasks = [];
for (const account of accounts) {
const loginResult = login(account.account, account.password, 'ADMIN');
if (loginResult === null) {
console.error(`[Setup] 로그인 실패: ${account.account}`);
continue;
}
const {storeId, accessToken} = loginResult;
// 5 ~ ${themes.size} 인 random 숫자 생성
const selectedThemes = selectRandomThemes(themes, randomIntBetween(5, themes.length));
for (const theme of selectedThemes) {
for (const date of dates) {
for (const time of theme.times) {
tasks.push({
storeId,
accessToken,
date,
time: time.startFrom,
themeId: theme.id,
});
}
}
}
}
console.log(`총 생성할 스케줄 수(iterations): ${tasks.length}`);
return {tasks};
}
export default function (data) {
// 👈 3. 현재 반복 횟수가 준비된 작업 수를 초과하는지 확인
const taskIndex = exec.scenario.iterationInTest;
if (taskIndex >= data.tasks.length) {
// 첫 번째로 이 조건에 도달한 VU가 테스트를 종료
if (taskIndex === data.tasks.length) {
console.log('모든 스케쥴 생성 완료. 테스트 종료');
exec.test.abort();
}
return;
}
const task = data.tasks[taskIndex];
if (!task) {
console.log(`[VU ${__VU}] 알 수 없는 오류: task가 없습니다. (index: ${taskIndex})`);
return;
}
createSchedule(task.storeId, task.accessToken, task);
}
function createSchedule(storeId, accessToken, schedule) {
const payload = JSON.stringify({
date: schedule.date,
time: schedule.time,
themeId: schedule.themeId,
});
const params = getHeaders(accessToken)
const res = http.post(`${BASE_URL}/admin/stores/${storeId}/schedules`, payload, params);
const success = check(res, {'일정 생성 성공': (r) => r.status === 200 || r.status === 201});
if (!success) {
console.error(`일정 생성 실패 [${res.status}]: 매장=${storeId}, ${schedule.date} ${schedule.time} (테마: ${schedule.themeId}) | 응답: ${res.body}`);
}
sleep(5)
return success;
}
function generateDates(days) {
const dates = [];
const today = new Date();
for (let i = 1; i <= days; i++) {
const date = new Date(today);
date.setDate(today.getDate() + i);
dates.push(date.toISOString().split('T')[0]);
}
return dates;
}
function selectRandomThemes(themes, count) {
const shuffled = [...themes].sort(() => 0.5 - Math.random());
return shuffled.slice(0, Math.min(count, themes.length));
}
export function teardown(data) {
if (data.tasks) {
console.log(`\n=== 테스트 완료: 총 ${data.tasks.length}개의 스케줄 생성을 시도했습니다. ===`);
} else {
console.log('\n=== 테스트 완료: setup 단계에서 오류가 발생하여 작업을 실행하지 못했습니다. ===');
}
}