Load testing without scripting: user journeys, variables, checks and thresholds
Most load testing tools want a scripting language: the test is a program, and writing and maintaining it is a developer's job. Yet most of a load test is the same few jobs every time: send a request, take a value from the response and put it into the next request, validate the response, wait, and compare the result against a threshold. This guide shows how each of those is done with a form, and where you really need code.
What does a load test script do?
- Requests: address, method, headers and body. A user journey is a list of requests: log in, list, add to cart, pay.
- Correlation: taking a value from a response (a token, an id, a CSRF value) and using it in later requests. Without it the journey breaks at the second step.
- Test data: every virtual user working with a different account and different products. Always the same data measures caches.
- Validation: a response that returns 200 with the wrong content is a failure too. You check the status code, a field in the body or the duration.
- Load model and think time: how many users, at what rate, for how long; real users pausing between steps (load test types).
- Thresholds: the criteria that say "passed" or "failed" at the end; p95 latency and error rate, for example (the p95 and p99 guide).
A form instead of a script
In a no-code editor each of these jobs is a field. What matters is whether the form can really do what a script does: without correlation a form only loads single requests; without data files every user shares one account; without thresholds the result is a chart, not a decision. When you look at a tool, ask these four: can it take values from responses, can it read test data from a file, can it validate the response's content, can it turn the result into a decision with a threshold.
With Spitfire
In Spitfire the test is built step by step in a web editor. The editor's tabs are the parts of a test: General & variables (description, variables, data files), Scenarios & steps, Thresholds, Environments, Options and JSON. Every change is validated on the server within a few hundred milliseconds and the load plan is previewed; the Try it button sends one iteration of the selected scenario and shows each step's request and response, so you check the journey without generating load.
- Steps: HTTP/HTTPS, WebSocket, SSE, gRPC, Kafka, MQTT, RabbitMQ, Redis, SQL and MongoDB, mixed in one scenario. A step like login is marked Run once per VU; if it fails, it is retried in the next iteration.
- Extraction: a value is taken from the response by JSONPath, XPath, regex (with a capture group), a header or the status code, and used in later steps as
{{token}}; a default can be given for when nothing matches. The editor suggests the variables extracted in earlier steps. - Built-in values:
{{__VU}}(the virtual user's number),{{__ITER}},{{$uuid}},{{$randInt 1 100}},{{$randString 16}},{{$timestamp}}and Turkish fake data:{{fake.tckn}},{{fake.iban}},{{fake.phone}},{{fake.fullName}},{{fake.email}},{{fake.address}}and more. - Data files: a CSV file is uploaded once and bound to the test under a name; its columns are used as
{{users.email}}. Rows are handed out sequentially, at random or uniquely (each row at most once per run); with several runners the sequential and unique modes split the rows between runners, so no two runners use the same row. - Checks: status code, duration, text in the body, a field by JSONPath or XPath; headers on HTTP, gRPC, RabbitMQ and Kafka, the row count on SQL and MongoDB. A failed check does not stop the iteration; it counts towards the checks rate.
- Load model: a constant or stepped number of virtual users, a constant or stepped arrival rate, or a fixed amount of work (shared or per-VU iterations); a random think time between steps; several scenarios running side by side and starting at different times.
- Thresholds: expressions such as
p(95)<500,rate<0.01,avg<200; on all requests, a step or a location. Stop when crossed stops the run when the threshold is crossed (after a delay if you like). A run that crosses a threshold ends as failed, and in CI the exit code says so (the CI/CD guide). - Environments: the same test for test, staging and production with their own variables and connections; when you start a run you pick the environment, a scale percentage and one-off values.
You do not have to build the test from scratch either: an OpenAPI/Swagger document, a Postman collection or a HAR file saved from the browser becomes steps (the OpenAPI and Postman guide); JMeter plans and k6 scripts can be imported (the migration guide).
A shop journey: every virtual user logs in once and takes the token, then in each iteration lists the products on a random page, takes the first product's id and adds it to the cart. It ramps to 50 users in 2 minutes and holds for 10. The p95 of all requests must stay under 500 ms and adding to the cart under 800 ms; if the error rate passes 1% after the first minute, the run stops. Below is this test, built in the editor, as its JSON tab shows it; the test was checked with spitfire validate.
{
"name": "Mağaza: giriş, ürün, sepet",
"variables": { "base": "https://shop.example.com", "password": "loadtest" },
"scenarios": [
{ "name": "alisveris",
"executor": { "type": "ramping-vus", "startVUs": 0,
"stages": [ { "duration": "2m", "target": 50 }, { "duration": "10m", "target": 50 },
{ "duration": "1m", "target": 0 } ] },
"steps": [
{ "id": "login", "name": "Giriş", "once": true,
"request": { "method": "POST", "url": "{{base}}/api/login",
"body": { "type": "json",
"content": "{\"email\":\"loadtest+{{__VU}}@example.com\",\"password\":\"{{password}}\"}" } },
"extract": [ { "var": "token", "from": "jsonpath", "expr": "$.token" } ],
"checks": [ { "type": "status", "value": 200 } ] },
{ "id": "products", "name": "Ürünleri listele",
"request": { "url": "{{base}}/api/products?page={{$randInt 1 20}}",
"headers": [ { "key": "Authorization", "value": "Bearer {{token}}" } ] },
"extract": [ { "var": "productId", "from": "jsonpath", "expr": "$.items[0].id" } ],
"checks": [ { "type": "status", "value": 200 },
{ "type": "jsonPath", "path": "$.items", "op": "exists" } ],
"thinkTime": { "min": "2s", "max": "5s" } },
{ "id": "cart", "name": "Sepete ekle",
"request": { "method": "POST", "url": "{{base}}/api/cart",
"headers": [ { "key": "Authorization", "value": "Bearer {{token}}" } ],
"body": { "type": "json", "content": "{\"productId\":\"{{productId}}\",\"quantity\":1}" } },
"checks": [ { "type": "status", "value": 201 },
{ "type": "latency", "op": "lt", "value": 800 } ],
"thinkTime": { "min": "1s", "max": "3s" } }
] }
],
"thresholds": [
{ "metric": "req_duration", "expr": "p(95)<500" },
{ "metric": "req_duration", "filter": { "step": "cart" }, "expr": "p(95)<800" },
{ "metric": "req_failed", "expr": "rate<0.01", "abortOnFail": true, "delayAbortEval": "1m" }
]
}The JSON tab is the whole test: edit it there and apply, and the form follows. The same file runs on the command line with spitfire run test.json and can live in version control; every save is a new version of the test.
What you cannot do without code
A scenario is a straight list of steps that runs from top to bottom in every iteration. Conditions ("skip this step if the cart is empty"), loops ("page through to the end"), custom metrics and arbitrary calculations on extracted values cannot be expressed, and no code runs in the test. Some of this can be built another way: once-only steps, separate scenarios starting at different times, extraction defaults and data file modes. If your journey really branches (some users pay, some only browse), making each branch its own scenario and setting their shares with VU counts is often more readable than a condition.
Spitfire installs on Docker or Kubernetes with one command; every testing feature and protocol is open in the free edition.