Behavior-Driven Development (BDD) is a software development approach that bridges the communication gap between business stakeholders and technical teams by using human-readable language to describe application behavior.
Cucumber is a popular BDD framework that uses the Gherkin syntax to define executable specifications. When integrated with Selenium, Cucumber allows testers and developers to write robust end-to-end UI tests in a natural language format.
How Page Objects integrate with Cucumber step definitions to separate UI structure from test logic in a declarative BDD framework.
Before you begin integrating Cucumber with Selenium, ensure you have the following installed:
Java (JDK 8+)
Maven
IDE (IntelliJ, Eclipse, or VS Code)
Cucumber Java
Selenium WebDriver
TestNG
src/test/java/
├── features/ # Gherkin feature files
├── stepdefinitions/ # Step definitions (glue code)
├── runners/ # Test runner using TestNG
├── pages/ # Page Objects (for POM)
├── hooks/ # Setup & teardown logic
<dependencies>
<!-- Cucumber -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>7.11.0</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-testng</artifactId>
<version>7.11.0</version>
</dependency>
<!-- Selenium -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.11.0</version>
</dependency>
<!-- TestNG -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Feature: Login functionality
Scenario: Successful login
Given the user is on the login page
When the user enters valid credentials
Then the user should land on the dashboard
This file defines test behavior in a way that's easy to read and map to actual automation code.
public class LoginSteps {
WebDriver driver = DriverManager.getDriver();
@Given("the user is on the login page")
public void navigateToLoginPage() {
driver.get("https://example.com/login");
}
@When("the user enters valid credentials")
public void enterCredentials() {
driver.findElement(By.id("username")).sendKeys("user");
driver.findElement(By.id("password")).sendKeys("pass");
driver.findElement(By.id("loginBtn")).click();
}
@Then("the user should land on the dashboard")
public void verifyDashboard() {
Assert.assertTrue(driver.getTitle().contains("Dashboard"));
}
}
@CucumberOptions(
features = "src/test/java/features",
glue = {"stepdefinitions", "hooks"},
plugin = {"pretty", "html:target/cucumber-reports"},
tags = "@SmokeTest"
)
public class TestRunner extends AbstractTestNGCucumberTests {}
Using AbstractTestNGCucumberTests enables running Cucumber scenarios with TestNG, which offers:
Parallel execution
Test grouping
Fine-grained reporting
Hooks let you define common pre/post conditions using Cucumber annotations.
public class Hooks {
WebDriver driver;
@Before
public void setUp() {
driver = new ChromeDriver();
DriverManager.setDriver(driver); // Utility to manage thread-safe driver
}
@After
public void tearDown() {
DriverManager.getDriver().quit();
}
}
You can also define:
@BeforeStep / @AfterStep for step-level actions
Conditional logic using scenario tags (e.g., take screenshots on failure)
Use Assert (TestNG or JUnit) or Hamcrest for validations
Generate reports using:
cucumber-html-reporter
ExtentReports
Allure
Scenario Outline: Invalid login attempts
Given the user is on the login page
When the user enters "<username>" and "<password>"
Then an error message should be displayed
Examples:
| username | password |
| wrong1 | test123 |
| fakeuser | pass456 |
The Page Object Model is a design pattern that creates an abstraction layer between the test code and the UI of an application. Each web page (or component) is represented as a separate class, encapsulating its elements and actions.
Promotes clean, reusable, and maintainable automation code by separating UI locators and actions from test logic.
✅ POM Example:
Example of a LoginPage class with actions
A Cucumber step definition calling POM methods
Emphasis on Gherkin → StepDef → PageObject flow
Tradeoffs: tight mapping, duplication, ease of understanding
public class LoginPage {
WebDriver driver;
public LoginPage(WebDriver driver) {
this.driver = driver;
}
By usernameField = By.id("username");
By passwordField = By.id("password");
By loginButton = By.id("loginBtn");
public void login(String username, String password) {
driver.findElement(usernameField).sendKeys(username);
driver.findElement(passwordField).sendKeys(password);
driver.findElement(loginButton).click();
}
}
In your test or step definition:
LoginPage loginpage = new LoginPage(driver);
loginpage.login("admin", "pass123");
Centralizes UI element locators
Encourages reuse across scenarios
Reduces maintenance when UI changes
Pairs well with BDD frameworks (Cucumber steps become cleaner)
In contrast to POM, the Command Pattern focuses on encapsulating actions as objects, making it easier to create modular, pluggable test steps or behaviors.
While Page Object Model (POM) focuses on UI structure, the Command Pattern encapsulates test actions (e.g., login, submit form, verify dashboard) as independent, reusable objects. This makes it ideal for:
✅ Keyword-driven frameworks
🔁 Reusable workflows (across different tests or apps)
🧠 Behavior orchestration for AI/ML testing, prompt evaluation, or data-driven pipelines
🧩 Dynamic test case construction using pluggable components
🧩 Command Pattern + Spring + Cucumber + Dynamic Gherkin Parsing + Auto Step Definition Generation
A modern, modular test automation architecture built for flexibility, reusability, and low-maintenance scale — ideal for SDETs working in fast-paced agile or AI-integrated environments.
This architecture combines modular automation design with smart runtime behavior — ideal for scalable, low-maintenance, and AI-friendly test frameworks.
Command Pattern:
Each user action is encapsulated in a Command class with execute() and setArgs(...). Promotes reusability and clean separation of behavior.
Spring Framework:
Handles dependency injection — wires drivers, services, utilities, and command beans automatically.
Cucumber:
Uses natural-language Gherkin syntax (Given, When, Then) to define behavior-driven test scenarios.
Dynamic Gherkin Parsing:
At runtime, Gherkin steps like "user logs in with {string} and {int}" are converted to regex, and arguments are extracted and typed automatically.
Auto Step Definition:
One universal step handler method matches all steps dynamically. No need to manually define step methods for every scenario.
✅ Goal
Enable a single Spring-managed class to contain multiple step methods, each annotated with @Gherkin("..."), and dynamically map incoming Gherkin steps to those methods at runtime.
Integrating the Command Pattern with the Spring Framework and auto-generated step definitions makes for a powerful, scalable automation architecture.
Below is a complete walkthrough and example showing how to:
Use Spring for dependency injection
Implement Command Pattern for pluggable test actions
Auto-generate or wire Cucumber Step Definitions dynamically using annotations and command registry
src/
├── commands/ # Command classes with reusable Gherkin step methods
│ └── UserCommands.java
├── core/ # Core engine for parsing and handling steps
│ ├── Gherkin.java
│ ├── GherkinStepParser.java
│ └── CommandRegistry.java
├── stepdefinitions/ # Step Definitions for Cucumber
│ └── DynamicStepDefs.java
├── runners/ # Test runners (e.g., JUnit, TestNG)
│ └── TestRunner.java
├── features/ # ✅ Gherkin feature files live here
│ └── user_login.feature # Example feature file
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>cucumber-spring-framework</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<cucumber.version>7.14.0</cucumber.version>
<spring.version>6.1.4</spring.version>
<junit.version>5.10.1</junit.version>
<selenium.version>4.21.0</selenium.version>
</properties>
<dependencies>
<!-- Cucumber -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-testng</artifactId>
<version>${cucumber.version}</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-spring</artifactId>
<version>${cucumber.version}</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Selenium -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>${selenium.version}</version>
</dependency>
<!-- WebDriverManager (optional but convenient) -->
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.7.0</version>
</dependency>
<!-- TestNG -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.9.0</version>
<scope>test</scope>
</dependency>
<!-- Cucumber HTML Report -->
<dependency>
<groupId>net.masterthought</groupId>
<artifactId>cucumber-reporting</artifactId>
<version>5.7.6</version>
</dependency>
<!-- Datatable support -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-datatable</artifactId>
<version>${cucumber.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Compiler Plugin -->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<!-- Surefire Plugin for TestNG -->
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<includes>
<include>**/TestRunner.java</include>
</includes>
<testFailureIgnore>false</testFailureIgnore>
</configuration>
</plugin>
</plugins>
</build>
</project>
🧩 1. @Gherkin Annotation (method-level)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Gherkin {
String value(); // e.g., "user logs in with {string} and {string}"
}
🧠 2. GherkinStepParser.java (Regex extractor)
public class GherkinStepParser {
private final Pattern pattern;
private final String template;
public GherkinStepParser(String template) {
this.template = template;
this.pattern = Pattern.compile(toRegex(template));
}
private String toRegex(String t) {
return t.replaceAll("\\{string}", "\"([^\"]*)\"")
.replaceAll("\\{int}", "(\\d+)")
.replaceAll("\\{word}", "(\\w+)");
}
public boolean matches(String stepText) {
return pattern.matcher(stepText).matches();
}
public List<Object> extractArgs(String stepText) {
List<Object> args = new ArrayList<>();
Matcher m = pattern.matcher(stepText);
if (m.find()) {
for (int i = 1; i <= m.groupCount(); i++) {
args.add(m.group(i));
}
}
return args;
}
}
⚙️ 3. CommandRegistry.java
@Component
public class CommandRegistry {
private final List<CommandEntry> entries = new ArrayList<>();
public CommandRegistry(ApplicationContext context) {
for (Object bean : context.getBeansWithAnnotation(Component.class).values()) {
for (Method method : bean.getClass().getDeclaredMethods()) {
if (method.isAnnotationPresent(Gherkin.class)) {
String template = method.getAnnotation(Gherkin.class).value();
entries.add(new CommandEntry(bean, method, new GherkinStepParser(template)));
}
}
}
}
public Optional<CommandEntry> match(String stepText) {
return entries.stream().filter(e -> e.parser.matches(stepText)).findFirst();
}
public record CommandEntry(Object bean, Method method, GherkinStepParser parser) {
public void invoke(String stepText) {
List<Object> args = parser.extractArgs(stepText);
try {
method.setAccessible(true);
method.invoke(bean, args.toArray());
} catch (Exception e) {
throw new RuntimeException("Error invoking step: " + stepText, e);
}
}
}
}
✅ 4. UserCommands.java
@Component
public class UserCommands {
@Autowired WebDriver driver;
@Gherkin("user logs in with {string} and {string}")
public void login(String username, String password) {
driver.get("https://example.com/login");
driver.findElement(By.id("username")).sendKeys(username);
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.id("loginBtn")).click();
}
@Gherkin("user logs out")
public void logout() {
driver.findElement(By.id("logoutBtn")).click();
}
@Gherkin("user should see welcome message")
public void verifyWelcome() {
assert driver.findElement(By.id("welcomeMsg")).getText().contains("Welcome");
}
}
🔁 5. DynamicStepDefs.java
package stepdefinitions;
import core.CommandRegistry;
import core.StepEventListener;
import io.cucumber.datatable.DataTable;
import io.cucumber.java.*;
import io.cucumber.java.en.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class DynamicStepDefs {
@Autowired
private CommandRegistry registry;
@Autowired(required = false)
private StepEventListener listener;
private long stepStartTime;
@BeforeStep
public void beforeStepHook(Scenario scenario) {
stepStartTime = System.currentTimeMillis();
if (listener != null) {
listener.before(scenario.getName());
}
}
@AfterStep
public void afterStepHook(Scenario scenario) {
long duration = System.currentTimeMillis() - stepStartTime;
if (listener != null) {
listener.after(scenario.getName(), duration, scenario.isFailed());
}
}
@Given(".*")
@When(".*")
@Then(".*")
public void handleStep(String stepText) {
executeStep(stepText);
}
@Given(".*")
@When(".*")
@Then(".*")
public void handleStepWithArgs(String stepText, DataTable table) {
executeStep(stepText, table);
}
private void executeStep(String stepText) {
try {
registry.match(stepText).ifPresentOrElse(
match -> {
match.invoke(stepText);
if (listener != null) {
listener.log("Step passed: " + stepText);
}
},
() -> {
String msg = "No match found for: " + stepText;
if (listener != null) listener.error(stepText, new RuntimeException(msg));
throw new RuntimeException(msg);
}
);
} catch (Exception e) {
if (listener != null) listener.error(stepText, e);
throw new RuntimeException("Step execution failed for: " + stepText, e);
}
}
private void executeStep(String stepText, DataTable table) {
List<List<String>> rawData = table.asLists();
// Here you can convert DataTable to args or inject into command as needed
executeStep(stepText); // or customize for DataTable-specific logic
}
}
🧪 6. TestRunner.java
@RunWith(Cucumber.class)
@CucumberOptions(
features = "src/test/resources/features",
glue = {"stepdefinitions"},
plugin = {"pretty", "html:target/cucumber-reports.html"}
)
public class TestRunner {}
The CLI Test Runner is a flexible, developer-friendly utility for executing automated tests from the command line — with or without Cucumber feature files.
It supports both:
✅ Gherkin-based execution using standard .feature files
✅ YAML/JSON-driven test flows mapped to @Gherkin methods (no .feature files needed)
Built on Apache Commons CLI, Spring, and the Command Pattern, the CLI Runner provides a fast and scriptable way to trigger tests in CI/CD pipelines, developer machines, or API-driven test orchestration systems.
🔧 Key Capabilities
🧩 Run Cucumber .feature files via --runFeatures=true
📜 Run YAML-based test flows with --flow=flow.yml
🌍 Select target environment: --env=dev | qa | prod
🧪 Choose browser: --browser=chrome | firefox
👁️🗨️ Enable headless or remote (grid/cloud) execution
📊 Generate CSV reports with step duration and status
🔄 Supports full Spring-powered command registry and dependency injection
Below is a complete working code template for the described 🚀 Command-Line Test Runner, combining:
Apache Commons CLI for argument parsing
Spring context for CommandRegistry
YAML-based flow execution
Optional Cucumber feature file support
WebDriver selection (Chrome/Firefox)
Headless mode
CSV reporting
✅ CLITestRunner.java
import com.fasterxml.jackson.databind.ObjectMapper;
import core.CommandRegistry;
import core.CommandRegistry.CommandEntry;
import io.cucumber.core.cli.Main;
import org.apache.commons.cli.*;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import utils.FlowLoader;
import utils.DriverFactory;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
public class CLITestRunner {
public static void main(String[] args) {
Options options = new Options();
options.addOption("flow", true, "Path to flow.yml");
options.addOption("env", true, "Environment (dev, qa, prod)");
options.addOption("browser", true, "Browser: chrome | firefox");
options.addOption("headless", true, "Enable headless: true | false");
options.addOption("report", true, "Report file path");
options.addOption("runFeatures", true, "Run Cucumber .feature files");
options.addOption("tags", true, "Cucumber tag filter (e.g. @Smoke)");
try {
CommandLine cmd = new DefaultParser().parse(options, args);
String flow = cmd.getOptionValue("flow", "flow.yml");
String env = cmd.getOptionValue("env", "dev");
String browser = cmd.getOptionValue("browser", "chrome");
boolean headless = Boolean.parseBoolean(cmd.getOptionValue("headless", "false"));
boolean runFeatures = Boolean.parseBoolean(cmd.getOptionValue("runFeatures", "false"));
String report = cmd.getOptionValue("report", "target/report.csv");
String tags = cmd.getOptionValue("tags", "");
System.setProperty("test.env", env);
System.setProperty("test.browser", browser);
System.setProperty("test.headless", String.valueOf(headless));
if (runFeatures) {
runCucumber(tags);
} else {
runYamlFlow(flow, report);
}
} catch (Exception e) {
System.err.println("❌ CLI Runner failed: " + e.getMessage());
e.printStackTrace();
}
}
private static void runCucumber(String tags) {
List<String> args = new ArrayList<>();
args.add("--glue");
args.add("stepdefinitions");
args.add("src/test/resources/features");
if (!tags.isEmpty()) {
args.add("--tags");
args.add(tags);
}
args.add("--plugin");
args.add("pretty");
args.add("--plugin");
args.add("html:target/cucumber-report.html");
Main.main(args.toArray(new String[0]));
}
private static void runYamlFlow(String flowFile, String reportPath) throws Exception {
List<String> steps = FlowLoader.loadSteps(flowFile);
List<StepResult> results = new ArrayList<>();
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("your.base.package")) {
CommandRegistry registry = context.getBean(CommandRegistry.class);
for (String step : steps) {
long start = System.currentTimeMillis();
boolean passed = false;
String error = "";
try {
Optional<CommandEntry> match = registry.match(step);
if (match.isPresent()) {
match.get().invoke(step);
passed = true;
} else {
throw new RuntimeException("No match for step: " + step);
}
} catch (Exception e) {
error = e.getMessage();
}
results.add(new StepResult(step, passed, System.currentTimeMillis() - start, error));
}
DriverFactory.quit();
}
generateCsvReport(results, reportPath);
}
private static void generateCsvReport(List<StepResult> results, String path) throws Exception {
List<String> lines = new ArrayList<>();
lines.add("Step,Passed,Duration(ms),Error");
for (StepResult r : results) {
lines.add(String.format("\"%s\",%s,%d,\"%s\"", r.step, r.passed, r.durationMs, r.error.replace("\"", "'")));
}
Files.write(Path.of(path), lines, StandardCharsets.UTF_8);
System.out.println("📊 Report written to: " + path);
}
record StepResult(String step, boolean passed, long durationMs, String error) {}
}
🧩 FlowLoader.java (YAML Step Loader)
import org.yaml.snakeyaml.Yaml;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
public class FlowLoader {
public static List<String> loadSteps(String yamlPath) {
try (InputStream input = new FileInputStream(yamlPath)) {
Yaml yaml = new Yaml();
Map<String, Object> data = yaml.load(input);
return (List<String>) data.get("steps");
} catch (Exception e) {
throw new RuntimeException("Failed to load YAML flow file", e);
}
}
}
✅ Example Usages
Run feature files:
java -jar test-runner.jar --runFeatures=true --tags="@Smoke"
Run YAML flow (no feature files):
java -jar test-runner.jar --flow=smoke.yml --browser=chrome --headless=true
In complex test flows—especially those spanning UI, API, or CLI-driven steps—commands often need to share data at runtime.
Whether it's an auth token, a generated user ID, or a status flag, passing this data cleanly between steps is critical for flexibility and maintainability.
The ContextDataHandler acts as a centralized, in-memory key-value store, enabling loosely coupled communication between commands without direct dependencies. It's lightweight, thread-safe, and fully compatible with both Cucumber and CLI runners.
Use it to bridge logic between steps while keeping your automation modular and scalable.
In modern test automation—especially when following the Command Pattern or building data-driven/AI-driven test flows—it’s essential for test steps to communicate without being tightly coupled.
The ContextDataHandler solves this by acting as a runtime data exchange — a lightweight, in-memory context store that commands can use to put, get, or share values dynamically during execution.
It enables scenarios like:
Storing a login token from an API call and using it in a UI step
Passing IDs (user, order, product) from one command to another
Sharing flags, status, or calculated values across CLI or Cucumber-based executions
Supporting multi-step workflows without binding steps to each other
Because it’s thread-safe and Spring-managed, ContextDataHandler integrates seamlessly with both feature-based execution and CLI test runners, making your automation cleaner, more modular, and easier to scale.
🔧 ContextDataHandler.java
@Component
public class ContextDataHandler {
private final Map<String, Object> context = new ConcurrentHashMap<>();
public void put(String key, Object value) {
context.put(key, value);
}
public <T> T get(String key, Class<T> type) {
return type.cast(context.get(key));
}
public Object get(String key) {
return context.get(key);
}
public boolean has(String key) {
return context.containsKey(key);
}
public void clear() {
context.clear();
}
public Set<String> keys() {
return context.keySet();
}
public void remove(String key) {
context.remove(key);
}
}
✅ Usage in Gherkin-annotated Commands
Step 1: Store data in one command
@Component
public class AuthCommand {
@Autowired ContextDataHandler context;
@Gherkin("user logs in via API and saves token")
public void loginAndStoreToken() {
String token = callLoginEndpoint(); // simulate API login
context.put("authToken", token);
}
}
Step 2: Use data in a different command
@Component
public class ResourceCommand {
@Autowired ContextDataHandler context;
@Gherkin("user accesses protected resource with stored token")
public void accessProtectedResource() {
String token = context.get("authToken", String.class);
callProtectedApi(token); // simulate token-based access
}
}
✅ Why Use ContextDataHandler?
🔁 Shares data (e.g., tokens, IDs) between steps without tight coupling
🧩 Keeps command classes modular and focused
⚙️ Works across both Cucumber and CLI test flows
🧠 Enables dynamic, data-driven test execution
🔐 Thread-safe for parallel or CI runs
📥 Supports preloading from YAML or exporting for debugging
✅ 1. Encapsulates Test Actions as Reusable Objects
Each test step (e.g., login, click, verify) is modeled as a self-contained command with its own logic, making it highly reusable and testable.
🔧 2. Supports Parameterization
Command objects can receive arguments dynamically at runtime (via setArgs() or method injection), enabling data-driven or AI-generated test flows.
🔁 3. Decouples Test Logic from Execution Flow
Commands can be triggered by Gherkin steps, YAML flows, or CLI input — without being tied to a specific runner like Cucumber.
📦 4. Pluggable and Composable
Commands can be registered and invoked dynamically, allowing composition of complex test scenarios from simple, reusable components.
🧠 5. Supports Dynamic Dispatch
Using a registry (like CommandRegistry), you can map test step strings to actual command implementations at runtime — ideal for keyword-driven and LLM-based testing.
🔄 6. Easy to Extend or Override (Method-Level @Gherkin)
New behaviors can be added without modifying the core framework or creating new step definitions.
You simply:
Add a new method in any Spring-managed class
Annotate it with @Gherkin("...")
It’s automatically registered and invokable at runtime
💡 7. Centralized Error Handling and Logging
Since execution is abstracted, you can wrap all commands with standard logging, timing, retry logic, and context tracking (e.g., DomainExecutor, StepEventListener).
📋 8. Framework-Agnostic Execution
Commands can be triggered:
From CLI
From .feature files (via dynamic step binding)
From external configs (e.g., YAML/JSON)
Even from an API or chatbot
🧪 9. Unit-Testable
Each command can be independently tested using standard JUnit/TestNG without full Cucumber context.
🧰 10. Scales Well for AI/ML Testing
Works naturally with:
Prompt-to-step generation
Dynamic parameter filling
LLM-driven workflows or custom DSLs