UNPKG

k6-cucumber-steps

Version:

Cucumber step definitions for running k6 performance tests.

338 lines (299 loc) 10.1 kB
// load_test_steps.js const { Given, When, Then } = require("@cucumber/cucumber"); const fs = require("fs"); const path = require("path"); const { execSync } = require("child_process"); const resolveBody = require("../lib/helpers/resolveBody.js"); const buildK6Script = require("../lib/helpers/buildK6Script.js"); const generateHeaders = require("../lib/helpers/generateHeaders.js"); const { generateK6Script, runK6Script } = require("../lib/utils/k6Runner.js"); require("dotenv").config(); const axios = require("axios"); // Validate thresholds (e.g., "rate<0.01") const validateThreshold = (threshold) => { const regex = /^[\w{}()<>:]+[<>=]\d+(\.\d+)?$/; if (!regex.test(threshold)) { throw new Error(`Invalid threshold format: ${threshold}`); } }; /** * @param {string} method - HTTP method (e.g., GET, POST). * @example * Given I set a k6 script for GET testing * Given I set a k6 script for POST testing */ Given("I set a k6 script for {word} testing", function (method) { this.config = { method: method.toUpperCase() }; }); /** * @param {DataTable} dataTable - Table with configurations. * @example * When I set to run the k6 script with the following configurations: * | virtual_users | duration | http_req_failed | http_req_duration | error_rate | stages | * | 10 | 5 | rate<0.05 | p(95)<200 | | | * | 50 | 10 | rate<0.01 | | rate<0.01 | [{"target": 10, "duration": "10s"}, {"target": 50, "duration": "30s"}] | */ When( "I set to run the k6 script with the following configurations:", function (dataTable) { const rawRow = dataTable.hashes()[0]; const row = {}; // Extract example values manually from this.pickle const exampleMap = {}; if (this.pickle && this.pickle.astNodeIds && this.gherkinDocument) { const scenario = this.gherkinDocument.feature.children.find((child) => { return child.scenario && child.scenario.examples?.length; }); const exampleValues = scenario?.scenario?.examples?.[0]?.tableBody?.[0]?.cells?.map( (cell) => cell.value ) || []; const exampleKeys = scenario?.scenario?.examples?.[0]?.tableHeader?.cells?.map( (cell) => cell.value ) || []; exampleKeys.forEach((key, idx) => { exampleMap[key] = exampleValues[idx]; }); } for (const [key, value] of Object.entries(rawRow)) { row[key] = value.replace(/<([^>]+)>/g, (_, param) => { return exampleMap[param] || value; }); } console.log("🚨 Resolved config row:", row); const validateThreshold = (value) => { const regex = /^[\w{}()<>:]+[<>=]\d+(\.\d+)?$/; if (value && !regex.test(value)) { throw new Error(`Invalid threshold format: ${value}`); } }; validateThreshold(row.http_req_failed); validateThreshold(row.http_req_duration); validateThreshold(row.error_rate); if (row.stages) { try { this.config.options = { stages: JSON.parse(row.stages), thresholds: { http_req_failed: [row.http_req_failed], http_req_duration: [row.http_req_duration], }, }; } catch { throw new Error("Invalid stages JSON format."); } } else { this.config.options = { vus: parseInt(row.virtual_users), duration: `${row.duration}s`, thresholds: { http_req_failed: [row.http_req_failed], http_req_duration: [row.http_req_duration], }, }; } if (row.error_rate) { this.config.options.thresholds.error_rate = [row.error_rate]; } } ); /** * @param {DataTable} dataTable - Table with header and value for request headers. * @example * When I set the request headers: * | Header | Value | * | Content-Type | application/json | * | Authorization | Bearer your_auth_token | */ When("I set the request headers:", function (dataTable) { const headers = {}; dataTable.hashes().forEach(({ Header, Value }) => { headers[Header] = Value; }); this.config.headers = { ...this.config.headers, // preserve auth headers if set ...headers, }; }); /** * @param {string} docString - Multiline string containing the endpoints. * @example * When I set the following endpoints used: * """ * /api/users * /api/products * """ */ When("I set the following endpoints used:", function (docString) { this.config.endpoints = docString .trim() .split("\n") .map((line) => line.trim()); }); /** * @param {string} method - HTTP method (e.g., POST, PUT). * @param {string} endpoint - The endpoint for the body. * @param {string} docString - Multiline string containing the request body (can use placeholders). * @example * When I set the following POST body is used for "/api/users" * """ * { * "username": "{{username}}", * "email": "{{faker.internet.email}}" * } * """ */ When( "I set the following {word} body is used for {string}", function (method, endpoint, docString) { this.config.method = method.toUpperCase(); this.config.body = resolveBody(docString, process.env); this.config.endpoint = endpoint; } ); When( "I use JSON payload from {string} for {word} to {string}", function (fileName, method, endpoint) { const allowedMethods = ["POST", "PUT", "PATCH"]; const methodUpper = method.toUpperCase(); if (!allowedMethods.includes(methodUpper)) { throw new Error( `Method "${method}" is not supported. Use one of: ${allowedMethods.join( ", " )}` ); } const basePayloadPath = this.parameters?.payloadPath || path.resolve(__dirname, "../../payloads"); const payloadPath = path.resolve(basePayloadPath, fileName); if (!fs.existsSync(payloadPath)) { throw new Error(`Payload file not found: ${payloadPath}`); } const rawTemplate = fs.readFileSync(payloadPath, "utf-8"); const resolved = resolveBody(rawTemplate, { ...process.env, ...(this.aliases || {}), }); this.config = { ...this.config, // ✅ Keep previously set options! method: methodUpper, endpoint, body: resolved, headers: this.config?.headers || {}, }; this.lastRequest = { method: methodUpper, endpoint, body: resolved, }; } ); /** * @param {string} authType - Authentication type (api_key, bearer_token, basic, none). * @example * When I set the authentication type to "bearer_token" * When I set the authentication type to "api_key" * When I set the authentication type to "basic" * When I set the authentication type to "none" */ When("I set the authentication type to {string}", function (authType) { this.config.headers = generateHeaders( authType, process.env, this.aliases || {} ); }); /** * Then I store the value at "data.token" as alias "token" */ Then( "I store the value at {string} as alias {string}", function (jsonPath, alias) { if (!this.lastResponse) { throw new Error("No previous response available."); } const pathParts = jsonPath.split("."); let value = this.lastResponse; for (const key of pathParts) { value = value?.[key]; if (value === undefined) break; } if (value === undefined) { throw new Error(`Could not resolve path "${jsonPath}" in the response`); } if (!this.aliases) this.aliases = {}; this.aliases[alias] = value; console.log(`🧩 Stored alias "${alias}":`, value); } ); When( "I login via POST to {string} with payload from {string}", async function (endpoint, fileName) { const basePayloadPath = this.parameters?.payloadPath || path.resolve(__dirname, "../../payloads"); const payloadPath = path.resolve(basePayloadPath, fileName); if (!fs.existsSync(payloadPath)) { throw new Error(`Payload file not found: ${payloadPath}`); } const rawTemplate = fs.readFileSync(payloadPath, "utf-8"); const resolved = resolveBody(rawTemplate, { ...process.env, ...(this.aliases || {}), }); try { const response = await axios.post( `${process.env.BASE_URL}${endpoint}`, resolved, { headers: { "Content-Type": "application/json", }, } ); this.lastResponse = response.data; // ✅ Makes aliasing work console.log("🔐 Login successful, response saved to alias context."); } catch (err) { console.error("❌ Login request failed:", err.message); throw new Error("Login request failed"); } } ); /** * @param {string} method - HTTP method of the request. * @example * Then I see the API should handle the GET request successfully * Then I see the API should handle the POST request successfully */ Then( "I see the API should handle the {word} request successfully", { timeout: 60000 }, async function (method) { if (!this.config || !this.config.method) { throw new Error("Configuration is missing or incomplete."); } const expectedMethod = method.toUpperCase(); const actualMethod = this.config.method.toUpperCase(); if (actualMethod !== expectedMethod) { throw new Error( `Mismatched HTTP method: expected "${expectedMethod}", got "${actualMethod}"` ); } try { const scriptContent = buildK6Script(this.config); const scriptPath = await generateK6Script( scriptContent, "load", process.env.K6_CUCUMBER_OVERWRITE === "true" ); const stdout = await runK6Script( scriptPath, process.env.K6_CUCUMBER_OVERWRITE === "true" ); console.log(stdout); } catch (error) { console.error("k6 execution failed:", error.message); throw new Error("k6 test execution failed"); } } );