Skip to content

Commit 643cf72

Browse files
committed
Some load testing for quiz
1 parent 37256d2 commit 643cf72

File tree

6 files changed

+664
-25
lines changed

6 files changed

+664
-25
lines changed

backend/Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,15 @@ loadtest-spike: loadtest-gen ## Run spike test only (500 VUs)
125125
loadtest-leaderboard: loadtest-gen ## Run leaderboard stress test only (100 RPS)
126126
@echo "Running leaderboard stress test..."
127127
@k6 run --env LEADERBOARD_RPS=100 --env LEADERBOARD_DURATION=5m ./cmd/loadtest/k6/leaderboard.js
128+
129+
loadtest-quiz: loadtest-gen ## Run quiz load test (20 VUs, 5 minutes)
130+
@echo "Running quiz load test..."
131+
@k6 run --env QUIZ_VUS=20 --env DURATION=5m ./cmd/loadtest/k6/quiz-scenario.js
132+
133+
loadtest-quiz-quick: loadtest-gen ## Run quick quiz load test (5 VUs, 1 minute)
134+
@echo "Running quick quiz load test..."
135+
@k6 run --env QUIZ_VUS=5 --env DURATION=1m ./cmd/loadtest/k6/quiz-scenario.js
136+
137+
loadtest-quiz-stress: loadtest-gen-all ## Run quiz stress test (10 RPS, 5 minutes)
138+
@echo "Running quiz stress test..."
139+
@k6 run --env QUIZ_RPS=10 --env STRESS_DURATION=5m ./cmd/loadtest/k6/quiz-scenario.js

backend/cmd/loadtest/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ make loadtest-quick
3939
| `make loadtest-stress` | Stress test: 200 VUs for 10 minutes |
4040
| `make loadtest-spike` | Spike test only: ramp 0 -> 500 -> 0 VUs |
4141
| `make loadtest-leaderboard` | Leaderboard stress: 100 requests/second |
42+
| `make loadtest-quiz` | Quiz load test: 20 VUs for 5 minutes |
43+
| `make loadtest-quiz-quick` | Quick quiz test: 5 VUs for 1 minute |
44+
| `make loadtest-quiz-stress` | Quiz stress test: 10 RPS for 5 minutes |
4245
| `make loadtest-gen` | Generate tokens only (1000 users) |
4346
| `make loadtest-gen-all` | Generate tokens for all users (up to 10k) |
4447

@@ -63,6 +66,21 @@ Focused testing of database-intensive leaderboard queries:
6366
- Constant 100 requests/second
6467
- 50% global standings, 30% local, 20% team
6568

69+
### 4. Quiz Load Test (`quiz-scenario.js`)
70+
Tests the complete quiz flow: get quiz details, start quiz, answer all questions, finalize.
71+
- `quiz_completion`: Steady load of users completing quizzes
72+
- `quiz_spike`: Spike test with many concurrent quiz takers
73+
- `quiz_stress`: High-frequency quiz completions
74+
75+
**Prerequisites for Quiz Tests:**
76+
1. Insert the test quiz into the database:
77+
```bash
78+
# Set your project and challenge IDs, then run:
79+
psql $DATABASE_URL -v project_id="'YOUR_PROJECT_ID'" -v challenge_id="'YOUR_CHALLENGE_ID'" \
80+
-f ./cmd/loadtest/scripts/insert_quiz.sql
81+
```
82+
2. The quiz ID defaults to `QZ01ARQN6LOADTEST00000QUIZ`, or set via `QUIZ_ID` env var
83+
6684
## Configuration
6785

6886
### Environment Variables
@@ -76,6 +94,10 @@ Focused testing of database-intensive leaderboard queries:
7694
| `LEADERBOARD_START` | 0s | When leaderboard test starts |
7795
| `LEADERBOARD_DURATION` | 5m | Duration of leaderboard test |
7896
| `LEADERBOARD_RPS` | 100 | Requests per second |
97+
| `QUIZ_ID` | QZ01LOADTESTQUIZ000000000000 | Quiz ID to test |
98+
| `QUIZ_VUS` | 20 | Virtual users for quiz test |
99+
| `QUIZ_RPS` | 10 | Requests per second for quiz stress test |
100+
| `STRESS_DURATION` | 5m | Duration of quiz stress test |
79101

80102
### Token Generator Flags
81103

@@ -127,6 +149,7 @@ cmd/loadtest/
127149
main.go # Go tool to generate JWT tokens
128150
k6/
129151
scenarios.js # Main test script
152+
quiz-scenario.js # Quiz-specific test scenarios
130153
lib/
131154
graphql.js # GraphQL HTTP helpers
132155
queries/
@@ -136,6 +159,9 @@ cmd/loadtest/
136159
standings-local.js
137160
standings-unit.js
138161
challenge.js
162+
quiz.js # Quiz query/mutation helpers
163+
scripts/
164+
insert_quiz.sql # SQL script to insert test quiz
139165
config.json # Generated tokens (gitignored)
140166
README.md
141167
```
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { graphqlRequest, checkGraphQLResponse, parseResponse } from '../lib/graphql.js';
2+
3+
// Query to get quiz details including all questions and their answers
4+
const GET_QUIZ_QUERY = `
5+
query GetQuiz($id: ID!) {
6+
quiz(id: $id) {
7+
id
8+
name
9+
description
10+
timeoutSeconds
11+
randomizeQuestions
12+
revealCorrectAnswers
13+
allowRetakes
14+
completionPoints
15+
userCanStart
16+
userActiveSubmission {
17+
id
18+
completedAt
19+
}
20+
questions {
21+
id
22+
questionText
23+
questionOrder
24+
... on PredefinedQuestion {
25+
allowMultipleSelection
26+
predefinedAnswers {
27+
id
28+
answerText
29+
answerOrder
30+
}
31+
}
32+
}
33+
}
34+
}
35+
`;
36+
37+
// Mutation to start a quiz (creates a submission)
38+
const START_QUIZ_MUTATION = `
39+
mutation StartQuiz($quizId: ID!) {
40+
startQuiz(quizId: $quizId) {
41+
id
42+
startedAt
43+
expiresAt
44+
questionOrder
45+
orderedQuestions {
46+
id
47+
questionText
48+
... on PredefinedQuestion {
49+
predefinedAnswers {
50+
id
51+
answerText
52+
answerOrder
53+
}
54+
}
55+
}
56+
}
57+
}
58+
`;
59+
60+
// Mutation to submit an answer
61+
const SUBMIT_ANSWER_MUTATION = `
62+
mutation SubmitQuizAnswer($submissionId: ID!, $input: SubmitQuizAnswerInput!) {
63+
submitQuizAnswer(submissionId: $submissionId, input: $input) {
64+
id
65+
answeredAt
66+
... on PredefinedResponse {
67+
selectedAnswerIds
68+
isCorrect
69+
}
70+
}
71+
}
72+
`;
73+
74+
// Mutation to finalize the quiz
75+
const FINALIZE_QUIZ_MUTATION = `
76+
mutation FinalizeQuiz($submissionId: ID!) {
77+
finalizeQuiz(submissionId: $submissionId) {
78+
id
79+
completedAt
80+
score
81+
maxScore
82+
scorePercentage
83+
pointsAwarded
84+
}
85+
}
86+
`;
87+
88+
/**
89+
* Get quiz details
90+
* @param {string} baseUrl - Base URL of the GraphQL API
91+
* @param {string} token - JWT token for authorization
92+
* @param {string} quizId - Quiz ID to fetch
93+
* @returns {object|null} Quiz data or null on error
94+
*/
95+
export function getQuiz(baseUrl, token, quizId) {
96+
const response = graphqlRequest(baseUrl, GET_QUIZ_QUERY, { id: quizId }, token, 'GetQuiz');
97+
if (checkGraphQLResponse(response, 'GetQuiz')) {
98+
const data = parseResponse(response);
99+
return data ? data.quiz : null;
100+
}
101+
return null;
102+
}
103+
104+
/**
105+
* Start a quiz submission
106+
* @param {string} baseUrl - Base URL of the GraphQL API
107+
* @param {string} token - JWT token for authorization
108+
* @param {string} quizId - Quiz ID to start
109+
* @returns {object|null} Submission data or null on error
110+
*/
111+
export function startQuiz(baseUrl, token, quizId) {
112+
const response = graphqlRequest(baseUrl, START_QUIZ_MUTATION, { quizId }, token, 'StartQuiz');
113+
if (checkGraphQLResponse(response, 'StartQuiz')) {
114+
const data = parseResponse(response);
115+
return data ? data.startQuiz : null;
116+
}
117+
return null;
118+
}
119+
120+
/**
121+
* Submit an answer for a question
122+
* @param {string} baseUrl - Base URL of the GraphQL API
123+
* @param {string} token - JWT token for authorization
124+
* @param {string} submissionId - Submission ID
125+
* @param {string} questionId - Question ID
126+
* @param {string[]} selectedAnswerIds - Array of selected answer IDs
127+
* @param {number} timeSpentSeconds - Time spent on the question
128+
* @returns {object|null} Response data or null on error
129+
*/
130+
export function submitAnswer(baseUrl, token, submissionId, questionId, selectedAnswerIds, timeSpentSeconds) {
131+
const input = {
132+
questionId,
133+
selectedAnswerIds,
134+
timeSpentSeconds,
135+
};
136+
const response = graphqlRequest(
137+
baseUrl,
138+
SUBMIT_ANSWER_MUTATION,
139+
{ submissionId, input },
140+
token,
141+
'SubmitQuizAnswer'
142+
);
143+
if (checkGraphQLResponse(response, 'SubmitQuizAnswer')) {
144+
const data = parseResponse(response);
145+
return data ? data.submitQuizAnswer : null;
146+
}
147+
return null;
148+
}
149+
150+
/**
151+
* Finalize a quiz submission
152+
* @param {string} baseUrl - Base URL of the GraphQL API
153+
* @param {string} token - JWT token for authorization
154+
* @param {string} submissionId - Submission ID to finalize
155+
* @returns {object|null} Finalized submission data or null on error
156+
*/
157+
export function finalizeQuiz(baseUrl, token, submissionId) {
158+
const response = graphqlRequest(baseUrl, FINALIZE_QUIZ_MUTATION, { submissionId }, token, 'FinalizeQuiz');
159+
if (checkGraphQLResponse(response, 'FinalizeQuiz')) {
160+
const data = parseResponse(response);
161+
return data ? data.finalizeQuiz : null;
162+
}
163+
return null;
164+
}
165+
166+
/**
167+
* Complete quiz flow: get quiz details, start, answer all questions, finalize
168+
* This function simulates a user taking a complete quiz
169+
*
170+
* @param {string} baseUrl - Base URL of the GraphQL API
171+
* @param {string} token - JWT token for authorization
172+
* @param {string} quizId - Quiz ID to complete
173+
* @param {boolean} correctAnswers - Whether to select correct answers (first answer) or random
174+
* @returns {object} Result with success status and submission data
175+
*/
176+
export function completeQuizFlow(baseUrl, token, quizId, correctAnswers = true) {
177+
// Step 1: Get quiz details
178+
const quiz = getQuiz(baseUrl, token, quizId);
179+
if (!quiz) {
180+
return { success: false, error: 'Failed to get quiz details' };
181+
}
182+
183+
// Check if user can start (hasn't already completed)
184+
if (!quiz.userCanStart) {
185+
return { success: true, skipped: true, reason: 'User already completed quiz' };
186+
}
187+
188+
// Step 2: Start the quiz
189+
const submission = startQuiz(baseUrl, token, quizId);
190+
if (!submission) {
191+
return { success: false, error: 'Failed to start quiz' };
192+
}
193+
194+
// Step 3: Answer all questions
195+
const questions = submission.orderedQuestions;
196+
for (let i = 0; i < questions.length; i++) {
197+
const question = questions[i];
198+
199+
// Get predefined answers if available
200+
const answers = question.predefinedAnswers;
201+
if (!answers || answers.length === 0) {
202+
continue; // Skip non-predefined questions for now
203+
}
204+
205+
// Select answer: first one (correct) or random
206+
let selectedAnswerId;
207+
if (correctAnswers) {
208+
// First answer is correct in our test data
209+
selectedAnswerId = answers[0].id;
210+
} else {
211+
// Random answer
212+
const randomIndex = Math.floor(Math.random() * answers.length);
213+
selectedAnswerId = answers[randomIndex].id;
214+
}
215+
216+
// Simulate thinking time (0.5-2 seconds)
217+
const timeSpent = Math.floor(Math.random() * 1500) + 500;
218+
219+
const answerResult = submitAnswer(
220+
baseUrl,
221+
token,
222+
submission.id,
223+
question.id,
224+
[selectedAnswerId],
225+
Math.floor(timeSpent / 1000)
226+
);
227+
228+
if (!answerResult) {
229+
return {
230+
success: false,
231+
error: `Failed to submit answer for question ${i + 1}`,
232+
submissionId: submission.id,
233+
};
234+
}
235+
}
236+
237+
// Step 4: Finalize the quiz
238+
const finalResult = finalizeQuiz(baseUrl, token, submission.id);
239+
if (!finalResult) {
240+
return { success: false, error: 'Failed to finalize quiz', submissionId: submission.id };
241+
}
242+
243+
return {
244+
success: true,
245+
submission: finalResult,
246+
score: finalResult.score,
247+
maxScore: finalResult.maxScore,
248+
pointsAwarded: finalResult.pointsAwarded,
249+
};
250+
}

0 commit comments

Comments
 (0)