This project is archived. New version - https://github.com/evpl/xteps2
High-level contextual steps in your tests for any reporting tool
Xteps is a facade that provides a convenient way to report test steps. Integrations with Allure, Qase, TestIT and ReportPortal are ready, but you can write your own listener for another reporting system or just create an issue.
Requires Java 8+ version or Kotlin JVM. Just add suitable dependency.
Maven:
<dependency>
<groupId>com.plugatar.xteps</groupId>
<artifactId>{artifact name from table}</artifactId>
<version>5.8</version>
<scope>test</scope>
</dependency>Gradle:
dependencies {
testImplementation 'com.plugatar.xteps:{artifact name from table}:5.8'
}Kotlin DSL:
dependencies {
testImplementation("com.plugatar.xteps:{artifact name from table}:5.8")
}Read more about checked exceptions and integrations.
First part of Xteps API is a set of step, stepTo and threadHook static methods located in the
com.plugatar.xteps.checked.Xteps / com.plugatar.xteps.unchecked.UncheckedXteps class.
classExampleTest {
@TestvoidstaticMethodsExample() {
WebDriverdriver = stepTo("Create WebDriver", () -> {
/* ... */returnnewChromeDriver();
});
threadHook(driver::quit);
step("Login", () -> {
step("Open page", () ->
driver.navigate().to("https://.../login")
);
step("Type login", () ->
driver.findElement(By.id("login_field")).sendKeys("user123")
);
step("Type password", () ->
driver.findElement(By.id("password_field")).sendKeys("1234567890")
);
step("Click on login button", () ->
driver.findElements(By.id("login_button"))
);
});
finalConnectiondbConnection = stepTo("Get database connection", () -> {
/* ... */returngetDatabaseConnection();
});
threadHook(dbConnection::close);
step("Check user full name label", () -> {
finalByusernameLabelLocator = By.id("full_name_label");
newWebDriverWait(driver, Duration.ofSeconds(5)).until(visibilityOfElementLocated(usernameLabelLocator));
finalStringdatabaseFullName = dbConnection.createStatement()
.executeQuery("select full_name from users where username = user123")
.getString("full_name");
finalStringuiFullName = driver.findElement(usernameLabelLocator).getText();
assertEquals(uiFullName, databaseFullName);
});
}
}Second part is a steps chain starts with stepsChain, stepsChainOf, stepsChainOf(T), stepsChainOf(T1, T2) and
stepsChainOf(T1, T2, T3) static method located in the com.plugatar.xteps.checked.Xteps /
com.plugatar.xteps.unchecked.UncheckedXteps class.
classExampleTest {
@TestvoidstepsChainMethodExample() {
stepsChain()
.stepToCtx("Create WebDriver", () -> {
/* ... */returnnewChromeDriver();
})
.chainHook(WebDriver::quit)
.nestedSteps("Login", chain -> chain
.step("Open page", driver ->
driver.navigate().to("https://.../login")
)
.step("Type login", driver ->
driver.findElement(By.id("login_field")).sendKeys("user123")
)
.step("Type password", driver ->
driver.findElement(By.id("password_field")).sendKeys("1234567890")
)
.step("Click on login button", driver ->
driver.findElements(By.id("login_button"))
)
)
.stepToCtx("Get database connection", () -> {
/* ... */returngetDatabaseConnection();
})
.chainHook(Connection::close)
.step("Check user full name label", (dbConnection, driver) -> {
finalByusernameLabelLocator = By.id("full_name_label");
newWebDriverWait(driver, Duration.ofSeconds(5)).until(visibilityOfElementLocated(usernameLabelLocator));
finalStringdatabaseFullName = dbConnection.createStatement()
.executeQuery("select full_name from users where username = user123")
.getString("full_name");
finalStringuiFullName = driver.findElement(usernameLabelLocator).getText();
assertEquals(uiFullName, databaseFullName);
})
.callChainHooks();
}
}Third part is a set of steps objects located in the com.plugatar.xteps.checked.stepobject /
com.plugatar.xteps.unchecked.stepobject package.
classCreateWebDriverextendsSupplierStep<WebDriver> {
publicCreateWebDriver() {
super("Create WebDriver", () -> {
/* ... */returnnewChromeDriver();
});
}
}
classOpenPageextendsConsumerStep<WebDriver> {
publicOpenPage(Stringurl) {
super("Open page: " + url, driver ->
driver.navigate().to(url)
);
}
}
classTypeLoginextendsConsumerStep<WebDriver> {
publicTypeLogin(Stringlogin) {
super("Type login: " + login, driver ->
driver.findElement(By.id("login_field")).sendKeys(login)
);
}
}
classTypePasswordextendsConsumerStep<WebDriver> {
publicTypePassword(Stringpassword) {
super("Type password: " + password, driver ->
driver.findElement(By.id("password_field")).sendKeys(password)
);
}
}
classClickOnLoginButtonextendsConsumerStep<WebDriver> {
publicClickOnLoginButton() {
super("Click on login button", driver ->
driver.findElements(By.id("login_button"))
);
}
}
classDatabaseConnectionextendsSupplierStep<Connection> {
publicDatabaseConnection() {
super("Get database connection", () -> {
/* ... */returngetDatabaseConnection();
});
}
}
classCheckUserFullNameextendsBiConsumerStep<Connection, WebDriver> {
publicCheckUserFullName(Stringusername) {
super("Check user full name", (dbConnection, driver) -> {
finalByusernameLabelLocator = By.id("full_name_label");
newWebDriverWait(driver, Duration.ofSeconds(5)).until(visibilityOfElementLocated(usernameLabelLocator));
finalStringdatabaseFullName = dbConnection.createStatement()
.executeQuery("select full_name from users where username = " + username)
.getString("full_name");
finalStringuiFullName = driver.findElement(usernameLabelLocator).getText();
assertEquals(uiFullName, databaseFullName);
});
}
}
publicclassExampleTest {
@TestvoidstepObjectsExample() {
stepsChain()
.stepToCtx(newCreateWebDriver())
.chainHook(WebDriver::quit)
.step(newOpenPage("https://.../login"))
.step(newTypeLogin("user123"))
.step(newTypePassword("1234567890"))
.step(newClickOnLoginButton())
.stepToCtx(newDatabaseConnection())
.chainHook(Connection::close)
.step(newCheckUserFullName("user123"))
.callChainHooks();
}
}Steps can be combined.
classLoginAsextendsConsumerStep<WebDriver> {
publicLoginAs(Stringusername, Stringpassword) {
super("Login as " + username + " / " + password, driver ->
stepsChainOf(driver)
.step(newOpenPage("https://.../login"))
.step(newTypeLogin(username))
.step(newTypePassword(password))
.step(newClickOnLoginButton())
);
}
}You can use stubs before actually implementing the steps.
@TestvoiddummyStepObjectsExample() {
stepsChain()
.stepToCtx(SupplierStep.<WebDriver>dummy("Create WebDriver"))
.chainHook(WebDriver::quit)
.step(RunnableStep.dummy("Open page https://.../login"))
.step(RunnableStep.dummy("Type login user123"))
.step(RunnableStep.dummy("Type password 1234567890"))
.step(RunnableStep.dummy("Click on login button"))
.stepToCtx(SupplierStep.<Connection>dummy("Get database connection"))
.chainHook(Connection::close)
.step(RunnableStep.dummy("Check user full name"))
.callChainHooks();
}BDD style example.
classOpenPageextendsConsumerStep<WebDriver> {
publicOpenPage(Stringurl) {
super("I open page " + url, driver ->
driver.navigate().to(url)
);
}
}
/*... */publicclassExampleTest {
@TestvoidbddStyleExample() {
stepsChain()
.withCtx(() -> {
/* ... */returnnewChromeDriver();
})
.chainHook(WebDriver::quit)
.step("When", newOpenPage("https://.../login"))
.step("And", newTypeLogin("user123"))
.step("And", newTypePassword("1234567890"))
.step("And", newClickOnLoginButton())
.step("Then", newUserFullNameIs("Expected Name"))
.callChainHooks();
}
}There are two ways to load parameters. Be aware that higher source override lower one - properties from file can be overridden by system properties.
| Priority | Source |
|---|---|
| 1 | System properties |
| 2 | Properties file (xteps.properties) |
| Name | Type | Required | Default value | Description |
|---|---|---|---|---|
| xteps.enabled | Boolean | No | true | Enable/disable steps logging. |
| xteps.spi | Boolean | No | true | Enable/disable Service Provider Interface mechanism to detect and instantiate com.plugatar.xteps.base.StepListener implementations. Implementations should have zero-argument public constructor. |
| xteps.listeners | String | No | List of com.plugatar.xteps.base.StepListener implementations names in Class#getTypeName() format. Names should be separated by ,. Implementations should have zero-argument public constructor. | |
| xteps.cleanStackTrace | Boolean | No | true | Removes all stack trace lines about Xteps from any exception except XtepsException. |
| xteps.defaultHooksOrder | Enum | No | FROM_LAST | The order in which chain and thread hooks of the same priority will be called - FROM_FIRST / FROM_LAST. |
| xteps.threadHooksThreadInterval | Long | No | 100 | Interval between thread hooks daemon thread executions in milliseconds. |
| xteps.threadHooksThreadPriority | Integer | No | 5 | Thread hooks daemon thread priority in the range 1 to 10. |
xteps.properties file example:
xteps.enabled=true
xteps.spi=true
xteps.listeners=com.my.prj.StepListenerImpl1,com.my.prj.StepListenerImpl2
xteps.cleanStackTrace=true
xteps.defaultHooksOrder=FROM_LAST
xteps.threadHooksThreadInterval=100
xteps.threadHooksThreadPriority=5You can use hooks in a steps chain. Hooks will be called in case of any exception in steps chain or in case of
callChainHooks method call. You also can use hooks to release resources.
stepsChain().withCtx(newAutoCloseableImpl())
.chainHook(AutoCloseable::close)
.step("Step", ctx -> {
//...
})
.callChainHooks();You can use thread hooks. Hooks will be called after current thread is finished. You also can use hooks to release resources.
stepsChain().withCtx(newAutoCloseableImpl())
.threadHook(AutoCloseable::close)
.step("Step", ctx -> {
//...
});cleanStackTrace option is enabled by default. It allows to clear the stack trace from Xteps calls.
finalclassExampleTest {
@Testvoidtest() {
step("Step", () -> {
//...step("Nested step", () -> {
//...thrownewRuntimeException("Nested step exception");
});
});
}
}Stacktrace will look like this.
java.lang.RuntimeException: Nested step exception
at my.project.ExampleTest.lambda$null$0(ExampleTest.java:15)
at my.project.ExampleTest.lambda$test$1(ExampleTest.java:13)
at my.project.ExampleTest.test(ExampleTest.java:11)
...
Xteps is tolerant to checked exceptions. Each method allows you to throw generic exception, the type of this exception is defined by the step body.
@Testvoidtest() throwsMalformedURLException {
step("Step", () -> {
//...step("Nested step", () -> {
newURL("abc");
});
});
}You can choose a mechanism to cheat checked exceptions if you want to hide them. Xteps give you the built-in static
methods by com.plugatar.xteps.base functional interfaces.
@Testvoidtest() {
step("Step", () -> {
//...step("Nested step", ThrowingRunnable.unchecked(() -> {
newURL("abc");
}));
});
}Unchecked Xteps just hides any exceptions.
@Testvoidtest() {
step("Step", () -> {
//...step("Nested step", () -> {
newURL("abc");
});
});
}Set up your project with Allure / Qase / ReportPortal and then just add suitable Xteps dependency.
You can use step name or step description replacements by the way provided by Allure / Qase / TestIT / ReportPortal.
stepsChain()
.withCtx("value")
.withCtx(111)
.step("Step with context = {0} and second context = {1}", (integer, string) -> {
//...
});This step will be reported with name "Step with context = 111 and second context = value".
You can also use utility methods for Allure, Qase and TestIT - AllureStepUtils, QaseStepUtils, TestITStepUtils.
It allows you to change the step name and other step attributes at runtime.
You may run into a problem if you use Xteps and JDK 8. The issue is caused by generic exceptions.
stepsChain()
.nestedStepsTo("Step", chain -> chain
.step("Nested step", () -> {})
);This code can fail build with this exception java: unreported exception java.lang.Throwable; must be caught or declared to be thrown.
You can switch to JDK 9+ or switch to Unchecked Xteps or use functional interfaces static methods to hide any exceptions.
stepsChain()
.nestedStepsTo("Step", ThrowingFunction.unchecked(chain -> chain
.step("Nested step", () -> {})
));