When a test fails, the first thing you ask is "What was it doing when it failed?" The @Step annotation answers that question. Every method annotated with @Step appears in the Allure report as a numbered action. When a test fails at step 5 of 8, you know exactly where it broke without reading a single line of code.
The best place for @Step is on your page object methods. Not on test methods — on page object methods. Why? Because page objects represent user actions. "Enter username", "Click login", "Select account" — these are the steps you want to see in the report.
import io.qameta.allure.Step;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class LoginPage extends BasePage {
private final By usernameField = By.id("username");
private final By passwordField = By.id("password");
private final By loginButton = By.cssSelector("button[type='submit']");
private final By errorMessage = By.cssSelector(".alert-danger");
private final By forgotPasswordLink = By.linkText("Forgot Password?");
public LoginPage(WebDriver driver) {
super(driver);
}
@Step("Enter username: {username}")
public LoginPage enterUsername(String username) {
type(usernameField, username);
return this;
}
@Step("Enter password")
public LoginPage enterPassword(String password) {
type(passwordField, password);
return this;
}
@Step("Click Login button")
public DashboardPage clickLoginButton() {
click(loginButton);
return new DashboardPage(driver);
}
@Step("Login as user: {username}")
public DashboardPage loginAs(String username, String password) {
enterUsername(username);
enterPassword(password);
return clickLoginButton();
}
@Step("Check if error message is displayed")
public boolean isErrorMessageDisplayed() {
return isDisplayed(errorMessage);
}
@Step("Get error message text")
public String getErrorMessageText() {
return getText(errorMessage);
}
}Notice @Step("Enter password") does NOT include {password} in the template. Never log passwords, tokens, or any sensitive data in reports. Reports get shared, archived, and sometimes made public. The username is fine — the password is not.
When loginAs() calls enterUsername(), enterPassword(), and clickLoginButton(), Allure creates nested steps automatically. The report shows a tree structure:
Test: testValidLogin
|- Login as user: testuser
| |- Enter username: testuser
| |- Enter password
| |- Click Login button
|- Check if welcome message is displayed [PASSED]
|- Verify logged in username matches [PASSED]See how clean that is? If the test failed at "Click Login button", a developer immediately knows the issue is not with data entry — the button itself is the problem. Maybe the button is disabled, maybe a spinner is blocking it, maybe the locator changed. You have narrowed the debugging window from "something failed" to "this specific action failed".
public class FundTransferPage extends BasePage {
@Step("Select from account: {accountType}")
public FundTransferPage selectFromAccount(String accountType) {
selectByVisibleText(fromAccountDropdown, accountType);
return this;
}
@Step("Enter beneficiary account: {accountNumber}")
public FundTransferPage enterBeneficiaryAccount(String accountNumber) {
type(beneficiaryField, accountNumber);
return this;
}
@Step("Enter transfer amount: ${amount}")
public FundTransferPage enterAmount(String amount) {
type(amountField, amount);
return this;
}
@Step("Enter transfer remark: {remark}")
public FundTransferPage enterRemark(String remark) {
type(remarkField, remark);
return this;
}
@Step("Submit fund transfer")
public TransferConfirmationPage submitTransfer() {
click(submitButton);
return new TransferConfirmationPage(driver);
}
@Step("Perform fund transfer: {fromAccount} -> {beneficiary}, Amount: ${amount}")
public TransferConfirmationPage transferFunds(
String fromAccount, String beneficiary,
String amount, String remark) {
selectFromAccount(fromAccount);
enterBeneficiaryAccount(beneficiary);
enterAmount(amount);
enterRemark(remark);
return submitTransfer();
}
}Use the {paramName} syntax in @Step to include parameter values. The parameter name must match the method parameter name exactly. This makes the report self-documenting: "Enter transfer amount: $500" is much more useful than just "Enter transfer amount".
Sometimes you need to add a step inside a test method without creating a separate method. Use Allure.step() for this.
import io.qameta.allure.Allure;
@Test
public void testFundTransfer() {
Allure.step("Navigate to Fund Transfer page");
FundTransferPage transferPage = dashboard.goToFundTransfer();
Allure.step("Perform transfer of $500 to account 1234567890");
TransferConfirmationPage confirmation = transferPage
.transferFunds("Savings", "1234567890", "500", "Rent payment");
Allure.step("Verify transfer confirmation");
Assert.assertTrue(confirmation.isTransferSuccessful(),
"Transfer should be confirmed as successful");
Assert.assertEquals(confirmation.getTransferredAmount(), "$500.00");
}Q: Where do you put @Step annotations — in tests or page objects?
A: In page objects, not in tests. Page object methods represent user actions like "Enter username", "Click Submit", "Select account". These are the steps I want visible in the report. Putting @Step on test methods would just show high-level names like "testLogin" — not useful. With @Step on page objects, when a test fails, the report shows the exact user action that failed (e.g., "Click Login button"), and if a compound method like loginAs() calls multiple sub-methods, Allure creates nested steps automatically, giving a detailed action trace.
Key Point: Put @Step on page object methods, not test methods. This gives you a step-by-step action trace in the report — the single most useful debugging tool.