Answer
Dual-Channel BDD: Web UI + REST API from Same Feature File
The Business Need
CODE
Same business rule verified two ways:
"A user with ADMIN role can access the user management module"
Channel 1 (UI): Navigate to /admin/users → page loads
Channel 2 (API): GET /api/admin/users with ADMIN token → 200 OK
Channel 3 (API): GET /api/admin/users with USER token → 403 Forbidden
Running both channels from same Gherkin = consistency testing
Feature File — Channel-Tagged
Gherkin
@regression
Feature: Role-Based Access Control
@smoke @ui-channel
Scenario: Admin can access user management via UI
Given I am authenticated as role "ADMIN"
When I navigate to the user management section
Then I should have full access to the section
@smoke @api-channel
Scenario: Admin can access user management via API
Given I am authenticated as role "ADMIN"
When I request access to "user-management" via API
Then the API should respond with status 200
And the response should contain a list of users
@regression @api-channel
Scenario: Standard user cannot access admin APIs
Given I am authenticated as role "USER"
When I request access to "user-management" via API
Then the API should respond with status 403
And the response error should say "Access Denied"
Execution Context — Strategy Pattern
Java
// Channel interface
public interface ExecutionChannel {
void navigateTo(String section);
int checkAccess(String section);
String getResponseBody();
}
// UI Channel Implementation
public class UIChannel implements ExecutionChannel {
private final WebDriver driver;
public UIChannel(WebDriver driver) { this.driver = driver; }
@Override
public void navigateTo(String section) {
Map<String, String> urls = Map.of(
"user-management", "/admin/users",
"product-management", "/admin/products"
);
driver.get(ConfigReader.getBaseUrl() + urls.get(section));
}
@Override
public int checkAccess(String section) {
try {
navigateTo(section);
return new WebDriverWait(driver, Duration.ofSeconds(10))
.until(d -> d.findElement(By.css("[data-page='" + section + "']")))
!= null ? 200 : 403;
} catch (Exception e) { return 403; }
}
}
// API Channel Implementation
public class APIChannel implements ExecutionChannel {
private Response lastResponse;
private String authToken;
public void setToken(String token) { this.authToken = token; }
@Override
public void navigateTo(String section) {
// No-op for API channel — navigation = API call
}
@Override
public int checkAccess(String section) {
Map<String, String> endpoints = Map.of(
"user-management", "/api/admin/users",
"product-management", "/api/admin/products"
);
lastResponse = given()
.header("Authorization", "Bearer " + authToken)
.get(ConfigReader.getApiUrl() + endpoints.get(section));
return lastResponse.statusCode();
}
@Override
public String getResponseBody() {
return lastResponse.getBody().asString();
}
}
ScenarioContext — Channel Aware
Java
public class ScenarioContext {
public WebDriver driver;
public ExecutionChannel channel;
public String authToken;
public int lastStatusCode;
public SoftAssert softAssert = new SoftAssert();
}
Shared Step Definitions — Channel Agnostic
Java
public class AccessControlSteps {
private final ScenarioContext ctx;
@Before("@ui-channel")
public void initUIChannel() {
ctx.driver = DriverFactory.createDriver("chrome", true);
ctx.channel = new UIChannel(ctx.driver);
}
@Before("@api-channel")
public void initAPIChannel() {
ctx.channel = new APIChannel();
}
@Given("I am authenticated as role {string}")
public void authenticateAs(String role) {
ctx.authToken = ApiAuth.getTokenForRole(role);
if (ctx.channel instanceof APIChannel) {
((APIChannel) ctx.channel).setToken(ctx.authToken);
} else {
// UI: inject session cookie
ctx.driver.get(ConfigReader.getBaseUrl());
ctx.driver.manage().addCookie(
new Cookie("auth_token", ctx.authToken));
}
}
@When("I request access to {string} via API")
public void requestViaApi(String section) {
ctx.lastStatusCode = ctx.channel.checkAccess(section);
}
@Then("the API should respond with status {int}")
public void verifyStatus(int expected) {
ctx.softAssert.assertEquals(ctx.lastStatusCode, expected,
"Wrong status code for section access");
}
@Then("I should have full access to the section")
public void verifyUIAccess() {
ctx.softAssert.assertFalse(
ctx.driver.getPageSource().contains("403") ||
ctx.driver.getPageSource().contains("Access Denied"),
"User does not have UI access");
}
}
Benefits of This Approach
CODE
1. Single Gherkin source of truth for both channels
2. API tests verify backend behavior
3. UI tests verify what users actually see
4. Inconsistency detected: API returns 200 but UI shows error → real bug
5. API tests run 10x faster → quick smoke before UI suite
6. Feature parity: any API endpoint that works must also work in UI
