Test Automation: Seamless Integration of Tools and Frameworks
Integrate tools and frameworks like Spring Boot, Java 8, Cucumber, and Serenity into a test automation framework.
Join the DZone community and get the full member experience.
Join For FreeIn this article, I want to give you an overview of how well the latest technologies can be integrated seamlessly into a test automation framework. Test automation is also an integral part of the software development lifecycle, and we want our test automation framework to contain all the latest features with minimal effect.
In this article, we build a sample test automation framework that consists of tools like Spring Boot, Cucumber, Java 8, and Serenity to test a sample Calculator application.
There is a simple calculator application, which returns the sum of two numbers.
Sample Application:
package com.example.serenitycucumber;
public class Calculator {
public int add(int firstNumber, int secondNumber) {
return firstNumber + secondNumber;
}
}
Let's start with the designing of the test automation framework.
As shown above in the design depicture, the test automation framework consists of four major pillars.
- Spring Boot Test: Provides a number of utilities and annotations to help when writing test automation code like auto-configuration and dependency injection.
- Java 8 Lambda: Provides a clear and concise way to represent one method interface usieng an expression.
- Serenity BDD: Reporting is one of Serenity’s strengths. Serenity not only reports on whether a test passes or fails, but has live documentation on what it did in a step-by-step narrative format that includes test data and screenshots.
- Cucumber 4: Last 2-3 years, cucumber has changed a lot, it will be good if our test automation contains all the new cucumber features.
Let’s jump into the coding part of how we can integrate all these together in one test automation framework.
Test Automation Framework
For the first step, create a sample Maven Java project and under the source folder, create a new end-to-end folder, e2e, where we will place all our end-to-end test automation code.
Note: This e2e has to be added to the project classpath.
For the next step, add the jar dependencies mentioned below to a .pom file.
Maven Dependencies
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-core</artifactId>
<version>2.0.70</version>
<exclusions>
<exclusion>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-cucumber4</artifactId>
<version>1.0.21</version>
<exclusions>
<exclusion>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/net.serenity-bdd/serenity-spring -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-spring</artifactId>
<version>2.0.70</version>
<exclusions>
<exclusion>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java8</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>4.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<includes>
<include>TestRunner.java</include>
</includes>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>net.serenity-bdd.maven.plugins</groupId>
<artifactId>serenity-maven-plugin</artifactId>
<version>2.0.70</version>
<executions>
<execution>
<id>serenity-reports</id>
<phase>post-integration-test</phase>
<goals>
<goal>aggregate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-core</artifactId>
<version>2.0.70</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
BDD Feature File
The next step creates a feature file using Gherkin syntax (e.g. Given, When, Then) under the resources folder.
Every statement in the scenario starts with a keyword (like Given, And, When, Then) that serves a special meaning and eventually invokes a method from a Step Definition class.
BDD Step Definition
Next create a concrete calculator step definition Java class which extends the base step definition Java file.
Note: This Java file using a Java 8 method references the GIVEN, THEN, THEN
statement.
package com.example.serenitycucumber.stepdefinition;
import static org.junit.Assert.assertEquals;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.serenitycucumber.steps.CalculatorSteps;
import com.example.serenitycucumber.util.Context;
import cucumber.api.java8.En;
import net.serenitybdd.core.Serenity;
/**
*
* @author ravi kumar
* This is a step definition class for calculator functionality.
*
*/
public class CalculatorStepdefs extends BaseStepDefinition implements En {
@Autowired
CalculatorSteps calculatorStep;
@Autowired
private Context context;
public CalculatorStepdefs() {
Given("^I have a Calculator$", this::initializeCalculator);
When("^I add (\\d+) and (\\d+)$", this::testAdd);
Then("^the sum should be (\\d+)$", this::validateResult);
}
private void initializeCalculator() {
calculatorStep.setUpCalculator();
}
private void testAdd(final int firstNumber, final int secondNumber) throws Throwable {
final Integer actualSumValue = calculatorStep.whenAddTwoNumbers(firstNumber, secondNumber);
Serenity.recordReportData().withTitle("Actual add result").andContents(actualSumValue.toString());
context.setValue("actualSum",actualSumValue);
}
private void validateResult(final int result) throws Throwable {
assertEquals("The expected sum does not equal the actual sum", result, (int)context.getValue("actualSum"));
}
}
The next step creates a base step definition Java file where we put a Spring integration class-level rule and this step definition Java file will be extended by the other concrete step definition Java class.
Note: The @SpringBootTest
annotations load the Spring context and configuration file.
package com.example.serenitycucumber.stepdefinition;
import org.junit.ClassRule;
import org.springframework.boot.test.context.SpringBootTest;
import com.example.serenitycucumber.config.Config;
import net.serenitybdd.junit.spring.integration.SpringIntegrationClassRule;
/**
*
* @author ravi kumar
* This is a base step definition class used/extends by every other step definition class.
*
*/
@SpringBootTest(classes = Config.class)
public abstract class BaseStepDefinition {
@ClassRule
public static SpringIntegrationClassRule springIntegrationClassRule = new SpringIntegrationClassRule();
}
Serenity BDD
Next, create a separate layer of Java files where we will place complex test-related code.
package com.example.serenitycucumber.steps;
import com.example.serenitycucumber.Calculator;
/**
*
* @author ravi kumar
*
*/
public class CalculatorSteps {
private Calculator calculator;
public void setUpCalculator() {
this.calculator = new Calculator();
}
public Integer whenAddTwoNumbers(final int firstNumber, final int secondNumber) {
return calculator.add(firstNumber, secondNumber);
}
}
BDD Runner
Here, we define cucumber optional values and runner type @RunWith CucumberWithSerenity
and provide the facility to put everything together.
package com.example.serenitycucumber.runner;
import cucumber.api.CucumberOptions;
import cucumber.api.SnippetType;
import net.serenitybdd.cucumber.CucumberWithSerenity;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import com.example.serenitycucumber.config.Config;
/**
*
* @author ravi kumar
* This is a runner file where we specify the cucumber options parameter value.
*/
@RunWith(CucumberWithSerenity.class)
@CucumberOptions(
plugin = {"pretty", "json:build/reports/cucumber.json"},
features = "src/e2e/resources/features/",
snippets = SnippetType.CAMELCASE,
glue = {"com.example.serenitycucumber"}, // packages used for glue code, looked up in the classpath
tags = {"not @manual"} // security
)
@SpringBootTest(classes = Config.class)
public class TestRunner{
}
Spring Dependency Configuration
Now we put all the dependencies in one place, which helps us to manage the framework when it grows.
package com.example.serenitycucumber.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.serenitycucumber.steps.CalculatorSteps;
import com.example.serenitycucumber.util.Context;
/**
*
* @author ravi kumar
*
* This is a test configuration file where we placed all the bean.
*/
@Configuration
public class Config {
@Bean
@Qualifier("getCalculatorStep")
public CalculatorSteps getCalculatorStep() {
return new CalculatorSteps();
}
@Bean
@Qualifier("getContext")
public Context getContext() {
return new Context();
}
}
Here, we define the Cucumber hooks using lambda expressions.
package com.example.serenitycucumber.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.example.serenitycucumber.config.Config;
import cucumber.api.Scenario;
import cucumber.api.java8.En;
/**
*
* @author ravi kumar
*
*/
@SpringBootTest(classes = Config.class)
public class CucumberHooks implements En{
@Autowired
private Context context;
public CucumberHooks(){
Before((Scenario scenario) -> {
System.out.println("---- before scenario ----".concat(scenario.getName()));
context.clearContext();
});
After((Scenario scenario) -> {
System.out.println("---- after scenario ----".concat(scenario.getName()));
});
}
}
This is a kind of container where we store intermediate values and later use these values further down in the code.
package com.example.serenitycucumber.util;
import java.util.HashMap;
/**
*
* @author ravi kumar
* This is a scenario context class used to save intermediate values.
*
*/
public class Context {
private HashMap<String, Object> con;
public Context() {
con = new HashMap<>();
}
public Object getValue(String value) {
return con.get(value);
}
public void setValue(String key,Object value) {
con.put(key, value);
}
public void clearContext() {
con.clear();
}
public void removeValue(String key) {
con.remove(key);
}
}
Serenity Output Report
For reference, please see this Github link.
Further Reading
Opinions expressed by DZone contributors are their own.
Comments