Module 6: Checks Groups And Realistic Scenarios

1. Module introduction: from hitting a route to a scenario

Overview

Up to module 5 your load test had a very simple form: many VUs hitting one route with the same data. POST /quote with Focus/basic/3h, repeated thousands of times. With that you learned to generate load, to measure latency and throughput, to set limits that make the test pass or fail. All correct. But if you stop to look at what you measured, you'll notice something uncomfortable: that doesn't resemble what real users do. Nobody quotes the same room a thousand times in a row. People quote different rooms, for different hours, with different tiers; and an important fraction don't stop at the quote, but book —a second step that depends on the result of the first—. This module is the one that closes that gap. It's where the load test stops being a repetitive hammer and becomes a scenario that imitates real use.

To make that leap you need four new pieces, and this module installs them one by one. The first is using check() to verify correctness —not just that the API responds, but that it responds well: status 200 and the correct price_cents and the body with the expected shape—. The second is group(), to organize a multi-step script into named blocks and get per-step metrics. The third is parametrizing the data: stop always requesting the same thing and vary room/tier/hours with a list of data, so you don't hit a single hot route. And the fourth, the heart of the module, is correlation: extract a value from one response and use it in the next —the quote → book → confirm flow—. It closes with realistic think time, a pause with jitter so the virtual users don't all march to the same beat.

Connection to the module: this lesson is the map; it doesn't yet run the complete scenario —that starts in lesson 2 and culminates in lesson 8's project—. What it installs here is the structure and an infrastructure decision I declare in full. The multi-step scenario, with checks and correlation, is actually run in the Python generator against the canonical Reservo API, and you'll see its real output. The k6 constructs —check, group, SharedArray, the correlation with res.json('booking_id')— go as labeled content, faithful to the k6.io documentation, because k6 isn't installed in this environment. Everything we label as Python output was measured here with Python 3.14.0.

A theater script, not a repeated note

Think of it with an analogy. Until now your load test was like a musician playing a single note at full volume, a thousand times per second. With that you can measure useful things: how loud it sounds, whether the amplifier holds the volume, how hot the equipment gets. But nobody goes to a concert to hear a repeated note. Real music is a piece: different notes, in a sequence, where each measure depends on the previous, with silences that give it rhythm. Measuring how the equipment holds up playing a real piece —with its variety and its dependence between parts— tells you something the repeated note never could.

A realistic load test is that piece. Instead of hitting /quote with the same data, your scenario quotes a varied room, books using the price it was quoted, and confirms using the id the booking returned —a sequence where each step depends on the previous—. It verifies at each step that the response was correct (the checks), organizes the steps into named blocks (the groups), varies the input data in each iteration (the parametrization), and makes realistic pauses between steps (the think time). The result isn't "does it hold up a thousand times of the same note?", but "does it hold up the complete piece the users will really play?". That question is much more useful, and answering it is what this module is about.

A realistic load test doesn't hit a route with fixed data: it runs a multi-step scenario, with varied data, where each step depends on the previous (correlation), verifies correctness at each step (checks), and makes realistic pauses (think time). It's the difference between repeating a note and playing a piece.

The four pieces of a realistic scenario

Each piece has its name in k6 and its executable equivalent in Python. Here's the complete map from minute one; each lesson develops one row.

PieceWhat it addsIn k6 (content)You see it executed in Python
Correctness checkVerify the response is correct, not just that it arrivedcheck(res, { ... }) with several criteriaLessons 2, 3, 6, 8
GroupOrganize the steps and measure per stepgroup('quote', () => { ... })Lesson 4 (latency per group)
ParametrizationVary the data so you don't hit a hot routeSharedArray + random indexLesson 5 (real distribution)
CorrelationExtract a value and reuse it in the next stepres.json('booking_id') → next requestLessons 6, 8
Think time with jitterRealistic, desynchronized pause between stepssleep(Math.random() * n + m)Lesson 7 (with/without, distribution)

All four rest on what you already know. The check you saw in its basic form in module 2; here you put it to verify correctness for real. The group and the parametrization are new. The correlation is the piece that turns several isolated requests into a scenario with a thread. And the think time you've used as a fixed sleep; here you add jitter to it. None is magic: all can be written in twenty lines of Python, and that's why you'll run them for real.

This module's declaration: GET /booking/<id>

Here's the infrastructure decision, and I declare it openly because the guide's rule is to hide nothing. The canonical Reservo API —GET /rooms, POST /quote, POST /bookdoesn't change: it still returns 7500 for Focus/basic/3h and 6000 for Focus/pro/3h, identical to how you knew it. But the heart of this module is correlation, and to show it complete I need a third step that uses the booking_id /book returns. Quoting and booking are two steps; a third that confirms the booking by its id is what makes the correlation visible end to end.

That's why this module adds an endpoint to the same server, and I declare it in full:

Declared endpointWhat it doesWhat we use it for
GET /booking/<id>Looks up an already-created booking by its booking_id and returns its data (booking_id, confirmed, price_cents, room, tier, hours), or 404 if it doesn't exist.The third step of the correlation scenario: use the booking_id /book returned to confirm the booking exists.

It's an honest endpoint: it saves each booking /book creates in an in-memory table and returns it when you look it up by id. With it, the quote → book → confirm flow is complete and the correlation is really visible: the booking_id that comes out of step 2 goes into the URL of step 3, and if it were wrong (a made-up id), step 3 would return 404 and the check would fail. The canonical API isn't touched; only this lookup endpoint is added, declared here.

One more note about /book, also declared: in this module POST /book optionally accepts the price_cents you saw in the quote, to be able to show the first correlation (the price from /quote travels to /book). If you send it and it no longer matches the current price, the API responds 409 —a realistic scenario of "the price changed while you were booking"—. If you don't send it, it books anyway. It's an additive extension over the canonical /book, not a change to its contract.

The map of the eight lessons

This module goes from verifying a response well to chaining several steps with varied data to putting it all together in a realistic scenario on Reservo. Each lesson leaves a piece:

LessonWhat it installs
1. Introduction (this one)The four pieces of a scenario; the declared endpoint; the map
2. Correctness checkVerify status and value and body shape under load
3. A failing check vs an aborting thresholdThe check measures and continues; the threshold gives the verdict
4. group()Organize the steps and measure per group (content)
5. Parametrize dataVary room/tier/hours so you don't hit a hot route
6. CorrelationExtract and reuse: quote → book → confirm
7. Think time with jitterThe realistic pause and why it desynchronizes
8. Mini-projectThe complete scenario, executed, with the checks rate

In the whole guide, this module is where the test becomes realistic. Modules 2 through 5 gave you the machinery —the script, the metrics, the profiles, the thresholds—; this one puts it at the service of a scenario that imitates real use. And it prepares module 7, where you'll analyze the results of a test like this and run it in CI: to analyze a multi-step scenario, you first have to know how to build it, and that's what you install here.

The boundary: what's this module's and what's the others'

So you don't mix them up, here's the exact line, because it's easy to want to drag things here that already have their home:

TopicWhere it livesWhy not here
Checks, groups, parametrization, correlation, think timeThis module (6)
The anatomy of the script and the VUsModule 2 (already seen)Here we reuse them to build the scenario
The metrics (p95, RPS, error rate)Module 3 (already seen)The scenario produces them; measuring them in depth was module 3
The load profiles (stages, ramps)Module 4 (already seen)How the load rises and falls; here the load is flat and we vary the content
The thresholds (pass/fail verdict)Module 5 (reused in L3)Here the check measures; the threshold decides —we reuse it, we don't re-explain it
Analyzing results and running in CIModule 7The "after" of having a scenario that runs
The capstone (complete load test)Module 8Brings everything together; here only the scenario piece

The mechanical rule: if the question is "how do I make my test resemble real use —several steps, varied data, dependence between steps, correctness verification?", it's this module. If it's "how does the load rise and fall?", it's module 4. If it's "how do I make the test fail when something breaks?", it's module 5 (and we reuse it in lesson 3). If it's "what do I do with the results and how do I run it in CI?", it's module 7.

Common mistakes

Believing that hitting a route with fixed data is already "a realistic load test." What happens: someone launches a thousand VUs against /quote with Focus/basic/3h and concludes "the API holds up." Why it happens: generating volume gets confused with generating realistic use. How to detect it: if all your VUs request exactly the same thing and never chain a second step, you're measuring a path, not the system. How to fix it: parametrize the data (lesson 5) and chain the steps with correlation (lesson 6). A real user quotes different things and often books.

Verifying only that the API "responded" and not that it responded well. What happens: someone adds check(res, { 'status is 200': ... }) and considers themselves satisfied with that. Why it happens: a 200 feels like "all good." How to detect it: under stress, the API can respond 200 with a wrong price or a malformed body, and your test wouldn't notice. How to fix it: verify the value (correct price_cents) and the shape (that the key exists and is of the expected type) too. That's exactly lesson 2.

Making up the second step's data instead of correlating it. What happens: someone wants to test the quote → book → confirm flow, but in the confirm step uses a fixed or made-up booking_id instead of the one the booking returned. Why it happens: it seems easier to "hardcode" an id. How to detect it: the confirm step always fails (or worse, passes by chance against an id that exists for another reason), and you're not testing the real flow. How to fix it: extract the booking_id from /book's response and use it in the next step. That's correlation, and it's lesson 6.

Exercises

Exercise 1 — Which piece solves each problem? For each symptom of an unrealistic load test, say which module piece fixes it (correctness check, group, parametrization, correlation, or think time). (a) "All my VUs request the same room, so I measure only that route." (b) "My test says 200 but I don't know if the price was correct." (c) "I want to test the full quote → book flow, but I don't know how to pass the id from one step to the other." (d) "All my VUs request at exactly the same time, in artificial spikes."

See solution
  • (a) Parametrization (lesson 5): vary room/tier/hours with a list of data so you don't hit a hot route.
  • (b) Correctness check (lesson 2): verify not just the status but the value (correct price_cents) and the shape of the body.
  • (c) Correlation (lesson 6): extract the booking_id from /book's response and use it in the next step.
  • (d) Think time with jitter (lesson 7): a random pause that desynchronizes the VUs and avoids artificial spikes.

Exercise 2 — Why is GET /booking/<id> declared? In one or two sentences: (a) Why does this module add a booking-lookup endpoint instead of sticking with /quote and /book? (b) What would happen in the confirm step if the booking_id the scenario uses were wrong (made up)?

See solution
  • (a) Because the heart of the module is correlation, and to show it complete a third step that uses the booking_id returned by /book is needed. GET /booking/<id> is that step: it looks up the booking by the id that came out of the previous step, closing the quote → book → confirm flow.
  • (b) The endpoint would return 404 (the booking doesn't exist), and the confirm step's check (status is 200, id matches) would fail. That's exactly what makes correlation useful: using the real id the booking returned, not a made-up one, is what makes the next step work.

Exercise 3 — The boundary. A colleague says, about their Reservo load test: (a) "I want the load to ramp from 0 to 50 VUs." (b) "I want the test to fail if the p95 exceeds 500 ms." (c) "I want each VU to quote a different room and then book with the price it was quoted." Which of these three are this module's topic and which aren't? Name the correct module for the ones that aren't.

See solution
  • (a) Not this module's: ramping the load is the load profiles (stages, ramping-vus), a module 4 topic.
  • (b) Not this module's: making the test fail on exceeding a limit is a threshold, a module 5 topic (here we reuse it in lesson 3, but it's taught there).
  • (c) Yes, this module's: quoting a different room is parametrization (lesson 5) and booking with the quoted price is correlation (lesson 6). It's exactly the scenario we build here.

Summary and next step

In this lesson you built the module's map. A realistic load test stops hitting one route with one fixed data and runs a scenario: several steps where each depends on the previous. Four pieces make it possible —the correctness check (verify the response is correct, not just that it arrived), the group() (organize and measure per step), the parametrization (vary the data so you don't hit a hot route), and the correlation (extract a value and reuse it in the next step)— plus the think time with jitter for a realistic rhythm. It's the difference between repeating a note and playing a piece.

You met this module's declaration: on top of the same canonical Reservo API (which still returns 7500 and 6000, intact), GET /booking/<id> is added —look up a booking by its id— so the third step of the correlation flow is real; and POST /book now accepts, additively, the quoted price_cents to show the first correlation. And the boundary is clear: here we build the realistic scenario; the profiles (M4), the thresholds (M5, reused in L3), and the analysis + CI (M7) have their home apart.

Before moving on you should be able to: name the four pieces of a realistic scenario and what each adds; explain why hitting a route with fixed data isn't enough; and say in your own words what correlation is and why the module declares GET /booking/<id>. What comes next is starting to build. In lesson 2 you put check() to real work: not just verifying the API responded, but that it responded well —status 200 and correct price_cents and the body with the expected shape—, and you measure it by running three checks per response against the canonical API.

Resources

  • Scenarios and user flows in k6 — the official reference for how k6 models multi-step user scenarios, the frame of this whole module. How a test that imitates real use is structured.
  • Checks in k6 — the verification piece lesson 2 puts to work for correctness. The basis of the whole module.
  • Module 5 of this guide — Thresholds, pass/fail, and SLOs — the pass/fail verdict lesson 3 reuses to contrast with the check. If you're not clear on what a threshold is, review it.
  • http.server — Python documentation — the standard-library server the canonical Reservo API runs with, including the GET /booking/<id> endpoint declared in this module. How the load target is brought up.