Hi all, I will explain a sample test automation project with Selenium and Spring Boot in this article. It is a combination of the latest technologies in test automation. First, I will start with some Spring boot terminology, and then we will use them in our Selenium project. Our test site is an e-commerce website n11.com, and we will have scenarios for invalid login. Let’s Start!
Tech Stack
- Java 17
- Selenium 4.x
- JUnit 5.x (Jupiter)
- Cucumber 7.x
- Spring Boot 3.0.1
Summary of Spring Boot Features for the Selenium Automation Project
The project I will explain in this article will have Spring Boot’s features like Dependency Inversion, Spring profiles, Aspect-Oriented Programming (AOP), Parallel test execution, BDD with Cucumber, Selenium Grid, and more.
Spring Boot Quick Start Guide for Test Automation
First, we need to add the dependencies below in our pom.xml file.
<?xml version="1.0" encoding="UTF-8"?> <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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>org.swtestacademy</groupId> <artifactId>selenium-springboot</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <maven.surefire.version>3.0.0-M8</maven.surefire.version> <java.version>17</java.version> <selenium.version>4.8.0</selenium.version> <junit.jupiter.version>5.9.2</junit.jupiter.version> <junit.platform.version>1.9.2</junit.platform.version> <testng.version>7.7.1</testng.version> <lombok.version>1.18.24</lombok.version> <cucumber.version>7.10.1</cucumber.version> </properties> <dependencies> <!--Spring Boot--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <!--Selenium--> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>${selenium.version}</version> </dependency> <!--JUnit5--> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>${junit.jupiter.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>${junit.jupiter.version}</version> </dependency> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-launcher</artifactId> <version>${junit.platform.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-suite</artifactId> <version>${junit.platform.version}</version> <scope>test</scope> </dependency> <!--Cucumber--> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-java</artifactId> <version>${cucumber.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-spring</artifactId> <version>${cucumber.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-testng</artifactId> <version>${cucumber.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-junit</artifactId> <version>${cucumber.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-junit-platform-engine</artifactId> <version>${cucumber.version}</version> </dependency> <!--TestNG--> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>${testng.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>${maven.surefire.version}</version> <configuration> <reportFormat>plain</reportFormat> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> </plugins> </build> </project>
You can also start a spring boot project via https://start.spring.io/, and then you will add the other testing dependencies, which you can find in the pom.xml above.
Note: I have used 3.0.1 version and updated the pom.xml on 10 Jan 2023.
We need a spring boot application class in the main package below.
@SpringBootApplication() public class SpringSeleniumApplication { public static void main(String[] args) { SpringApplication.run(SpringSeleniumApplication.class, args); } }
Below are the Spring Boot annotations that I used in the project.
@Autowired: This annotation automatically creates the instances declared with @Component annotation with @Bean annotation. It does the dependency inversion-related operations behind the scenes.
@PostConstruct: It is a part of the spring bean life cycle, and it does the operations after the post-construction of the beans. @Postconstruct annotated method will be invoked after the bean has been constructed using the default constructor and just before its instance is returned to requesting object.
@Component: @Component is an annotation allowing Spring to detect our custom beans automatically.
@Lazy: @Lazy annotation indicates whether a bean is to be lazily initialized. It can be used on @Component and @Bean definitions. A @Lazy bean is not initialized until referenced by another bean or explicitly retrieved from BeanFactory. Beans that are not annotated with @Lazy are initialized eagerly.
@Bean: Spring @Bean annotation tells that a method produces a bean to be managed by the Spring container. It is a method-level annotation. During Java configuration (@Configuration), the method is executed, and its return value is registered as a bean within a BeanFactory.
@Configuration: @Configuration tags the class as a source of bean definitions for the application context.
@Value: Spring @Value annotation assigns default values to variables and method arguments. We can read spring environment variables and system variables using @Value annotation.
@Profile: This annotation loads the specific profiles. Profiles are a core feature of the framework, allowing us to map our beans to different profiles, for example, dev, test, stage, etc.
Custom Annotations
I used some custom annotations to gather multiple annotations in one annotation. Below are some examples of custom annotations.
@Lazy and @Autowired annotation combination, and I named it as @LazyAutowired:
@Lazy @Autowired @Documented @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface LazyAutowired { }
@Lazy and @Component annotation combination was named as @LazyComponent:
@Lazy @Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Documented @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface LazyComponent { }
@ElapsedTime – Spring AOP:
@Documented @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface ElapsedTime { }
@TakeScreenshot – Spring AOP:
@Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface TakeScreenshot { }
For configurations:
@Lazy @Configuration @Documented @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface LazyConfiguration { }
For Tests:
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(SpringExtension.class) @SpringBootTest(classes = SpringSeleniumApplication.class) public @interface SeleniumTest { }
For parallel test execution:
@Bean @Scope("webdriverscope") @Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface WebdriverScopeBean { }
Spring Boot Selenium Test Automation Classes
Let’s start with Configurations. We need to define the objects from external libraries as beans like WebDriver instances in the configurations.
WebdriverConfig Class
@Profile(“!grid”) tells us that this configuration works when the profile is not the grid.
@WebdriverScopeBean is for parallel test execution.
@ConditionalOnProperty annotation helps us use the specific beans based on the properties in the configuration properties file which is application.properties in the resource folder.
@Primary annotation gives a higher preference to a bean when there are multiple beans of the same type.
@ConditionalMissingBean annotation lets a bean be included based on the absence of specific beans.
@Profile("!grid") @LazyConfiguration public class WebDriverConfig { @WebdriverScopeBean @ConditionalOnProperty(name = "browser", havingValue = "firefox") @Primary public WebDriver firefoxDriver() { FirefoxOptions firefoxOptions = new FirefoxOptions(); Proxy proxy = new Proxy(); proxy.setAutodetect(false); proxy.setNoProxy("no_proxy-var"); firefoxOptions.setCapability("proxy", proxy); return new FirefoxDriver(firefoxOptions); } @WebdriverScopeBean @ConditionalOnProperty(name = "browser", havingValue = "edge") @Primary public WebDriver edgeDriver() { return new EdgeDriver(); } @WebdriverScopeBean @ConditionalOnMissingBean @ConditionalOnProperty(name = "browser", havingValue = "chrome") @Primary public WebDriver chromeDriver() { return new ChromeDriver(); } }
WebDriverWaitConfig Class
This class is for auto-wiring the WebdriverWait instance in the tests. The default timeout duration is set as 30 seconds, and for parallel test execution, the bean is annotated with @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE). A bean with the prototype scope will return a different instance every time it is requested from the container.
@LazyConfiguration public class WebDriverWaitConfig { @Value("${default.timeout:30}") private int timeout; @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public WebDriverWait webdriverWait(WebDriver driver) { return new WebDriverWait(driver, Duration.ofMillis(this.timeout)); } }
RemoteWebDriverConfig Class
@Profile(“grid”) annotation is for Selenium Grid and remotewebdriver. When we run the tests with “spring.profiles.active=grid” environment variable, the tests will use application-grid.properties file under the resources folder as the main configuration file.
@Profile("grid") @LazyConfiguration public class RemoteWebDriverConfig { @Value("${selenium.grid.url}") private URL url; @WebdriverScopeBean @ConditionalOnProperty(name = "browser", havingValue = "firefox") @Primary public WebDriver remoteFirefoxDriver(){ FirefoxOptions firefoxOptions = new FirefoxOptions(); return new RemoteWebDriver(this.url, firefoxOptions); } @WebdriverScopeBean @ConditionalOnProperty(name = "browser", havingValue = "edge") @Primary public WebDriver remoteEdgeDriver(){ EdgeOptions edgeOptions = new EdgeOptions(); return new RemoteWebDriver(this.url, edgeOptions); } @WebdriverScopeBean @ConditionalOnMissingBean @ConditionalOnProperty(name = "browser", havingValue = "chrome") @Primary public WebDriver remoteChromeDriver(){ ChromeOptions chromeOptions = new ChromeOptions(); return new RemoteWebDriver(this.url, chromeOptions); } }
OtherBeansConfigs Class
For the other beans, I used this class. The class below has the JavaScriptExecutor bean.
@LazyConfiguration public class OtherBeansConfigs { @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public JavascriptExecutor javascriptExecutor(WebDriver driver) { return (JavascriptExecutor) driver; } }
For parallel test execution, I have three scope classes as follows.
WebdriverScope Class
It extends the SimpleThreadScope based on the webdriver session and cleans the threadScope map. It is a ThreadLocal map inside SimpleThreadScope class.
public class WebdriverScope extends SimpleThreadScope { @Override public Object get(String name, ObjectFactory<?> objectFactory) { Object o = super.get(name, objectFactory); SessionId sessionId = ((RemoteWebDriver)o).getSessionId(); if(Objects.isNull(sessionId)){ super.remove(name); o = super.get(name, objectFactory); } return o; } @Override public void registerDestructionCallback(String name, Runnable callback) { } }
WebdriverScopeConfig Class
It is a configuration class, and it creates the WebdriverScopePostProcessor bean.
@Configuration public class WebdriverScopeConfig { @Bean public static BeanFactoryPostProcessor beanFactoryPostProcessor(){ return new WebdriverScopePostProcessor(); } }
WebdriverScopePostProcessor Class
This class implements the BeanFactoryPostProcessor functional interface and registers the scope as “webdriverscope” by using WebDriverScope class as an argument.
public class WebdriverScopePostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { beanFactory.registerScope("webdriverscope", new WebdriverScope()); } }
Let’s continue with Page Classes.
BasePage Class
This is the base class for all pages, and all pages extend the BasePage class. Here I autowired the WebDriver, WebDriverWait, JavascripExecutor, LogUtil classes and in this way, I can use the instances of these classes. Also, with @PostConstruct annotation after the creation of this page, I initiated the elements with “PageFactory.initElements(this.driver, this);” this line for PageFactory usage.
package com.swtestacademy.springbootselenium.pages; import com.swtestacademy.springbootselenium.utils.LogUtil; import jakarta.annotation.PostConstruct; import lombok.SneakyThrows; import org.openqa.selenium.By; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.PageFactory; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; public abstract class BasePage { @Autowired protected WebDriver driver; @Autowired protected WebDriverWait wait; @Autowired protected JavascriptExecutor javascriptExecutor; @Autowired protected LogUtil logUtil; @PostConstruct private void init() { PageFactory.initElements(this.driver, this); } public abstract boolean isAt(); public <T> void waitElement(T elementAttr) { if (elementAttr .getClass() .getName() .contains("By")) { wait.until(ExpectedConditions.presenceOfElementLocated((By) elementAttr)); } else { wait.until(ExpectedConditions.visibilityOf((WebElement) elementAttr)); } } public <T> void waitElements(T elementAttr) { if (elementAttr .getClass() .getName() .contains("By")) { wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy((By) elementAttr)); } else { wait.until(ExpectedConditions.visibilityOfAllElements((WebElement) elementAttr)); } } //Click Method by using JAVA Generics (You can use both By or Web element) public <T> void click(T elementAttr) { waitElement(elementAttr); if (elementAttr .getClass() .getName() .contains("By")) { driver .findElement((By) elementAttr) .click(); } else { ((WebElement) elementAttr).click(); } } public void jsClick(By by) { javascriptExecutor.executeScript("arguments[0].click();", wait.until(ExpectedConditions.visibilityOfElementLocated(by))); } //Write Text by using JAVA Generics (You can use both By or WebElement) public <T> void writeText(T elementAttr, String text) { waitElement(elementAttr); if (elementAttr .getClass() .getName() .contains("By")) { wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy((By) elementAttr)); driver .findElement((By) elementAttr) .sendKeys(text); } else { wait.until(ExpectedConditions.visibilityOf((WebElement) elementAttr)); ((WebElement) elementAttr).sendKeys(text); } } //Read Text by using JAVA Generics (You can use both By or WebElement) public <T> String readText(T elementAttr) { if (elementAttr .getClass() .getName() .contains("By")) { return driver .findElement((By) elementAttr) .getText(); } else { return ((WebElement) elementAttr).getText(); } } @SneakyThrows public <T> String readTextErrorMessage(T elementAttr) { Thread.sleep(2000); //This needs to be improved. return driver .findElement((By) elementAttr) .getText(); } //Close popup if exists public void handlePopup(By by) throws InterruptedException { waitElements(by); List<WebElement> popup = driver.findElements(by); if (!popup.isEmpty()) { popup .get(0) .click(); Thread.sleep(200); } } }
HomePage Class
Homepage class extends the BasePage class and does the related operations for the Homepage of n11.com like “goToHomePage()” and “goToLoginPage()”. I also overrode the isAt method of the BasePage class to wait for the HomePage class to load completely.
@LazyComponent public class HomePage extends BasePage { @Autowired LoginPage loginPage; @Value("${application.url}") private String baseURL; //*********Web Elements By Using Page Factory********* @FindBy(how = How.CLASS_NAME, using = "btnSignIn") public WebElement signInButton; //*********Page Methods********* //Go to Homepage public HomePage goToHomePage() { driver.get(baseURL); return this; } //Go to LoginPage public HomePage goToLoginPage() { click(signInButton); return this; } @Override public boolean isAt() { return this.wait.until((d) -> this.signInButton.isDisplayed()); } }
LoginPage Class
LoginPage class extends the BasePage class and does the login page-related operations.
@LazyComponent public class LoginPage extends BasePage { //********* Web Elements by using Page Factory ********* @FindBy(how = How.ID, using = "email") public WebElement userName; @FindBy(how = How.ID, using = "password") public WebElement password; //********* Web Elements by using By Class ********* By loginButtonBy = By.id("loginButton"); By errorMessageUsernameBy = By.xpath("//*[@id=\"loginForm\"]/div[1]/div/div"); By errorMessagePasswordBy = By.xpath("//*[@id=\"loginForm\"]/div[2]/div/div"); By errorMessagePasswordCssBy = By.cssSelector("div[data-errormessagefor='password'] > .errorText"); //*********Page Methods********* public LoginPage login(String userName, String password) { writeText(this.userName, userName); writeText(this.password, password); jsClick(loginButtonBy); return this; } public LoginPage verifyLoginUserNameErrorMessage(String expectedText) { assertEquals(expectedText, readText(errorMessageUsernameBy)); return this; } public LoginPage verifyPasswordErrorMessage(String expectedText) { assertEquals(expectedText, readText(errorMessagePasswordBy)); return this; } public LoginPage verifyPasswordErrorMessageWithCss(String expectedText) { assertEquals(expectedText, readTextErrorMessage(errorMessagePasswordCssBy)); return this; } public LoginPage verifyLogEntryFailMessage() { logUtil.isLoginErrorLog(driver); return this; } @Override public boolean isAt() { return this.wait.until((d) -> this.userName.isDisplayed()); } }
We will continue with the Steps class, and we have only LoginSteps class for our scenario.
LoginSteps Class
As seen below, in LoginSteps class, I auto-wired the page classes and defined the scenario methods. The @ElapsedTime and @TakeScreenshot annotations are Spring AOP (aspect-oriented programming) annotations which I will share their details later. I also read the browser value from the configuration properties by using @Value annotation of the spring framework.
@LazyComponent public class LoginSteps { @Value("${browser}") private String browser; @LazyAutowired HomePage homePage; @LazyAutowired LoginPage loginPage; public LoginSteps givenIAmAtLoginPage() { homePage .goToHomePage() .goToLoginPage(); return this; } @ElapsedTime public LoginSteps whenILogin(String userName, String password) { loginPage .login(userName, password); return this; } public LoginSteps thenIVerifyUserNameErrorMessages(String expected) { loginPage .verifyLoginUserNameErrorMessage(expected); return this; } @TakeScreenshot public LoginSteps thenIVerifyInvalidLoginMessage() { if(!browser.equalsIgnoreCase("firefox")) { loginPage .verifyLogEntryFailMessage(); } else { loginPage.verifyPasswordErrorMessageWithCss("E-posta adresiniz veya şifreniz hatalı"); } return this; } @TakeScreenshot public LoginSteps thenIVerifyPasswordErrorMessage(String expected) { loginPage .verifyPasswordErrorMessage(expected); return this; } @TakeScreenshot public LoginSteps thenIVerifyPasswordErrorMessageWithCss(String expected) { loginPage .verifyPasswordErrorMessageWithCss(expected); return this; } }
I created some util classes like LogUtil, ScreenshotUtil, WindowSwitchUtil, etc. You can find all of them on the GitHub page of the project.
BrowserOps Class
This class is for preparing and getting the browser-specific options.
package com.swtestacademy.springbootselenium.utils; import com.swtestacademy.springbootselenium.annotations.LazyComponent; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.firefox.FirefoxOptions; import org.openqa.selenium.firefox.FirefoxProfile; import org.openqa.selenium.logging.LogType; import org.openqa.selenium.logging.LoggingPreferences; import java.util.logging.Level; @LazyComponent public class BrowserOps { public ChromeOptions getChromeOptions() { ChromeOptions chromeOptions = new ChromeOptions(); LoggingPreferences logPrefs = new LoggingPreferences(); logPrefs.enable(LogType.BROWSER, Level.ALL); logPrefs.enable(LogType.DRIVER, Level.ALL); chromeOptions.setCapability("goog:loggingPrefs", logPrefs); return chromeOptions; } public FirefoxOptions getFireFoxOptions() { FirefoxProfile firefoxProfile = new FirefoxProfile(); firefoxProfile.setPreference("devtools.console.stdout.content", true); FirefoxOptions firefoxOptions = new FirefoxOptions(); LoggingPreferences logPrefs = new LoggingPreferences(); logPrefs.enable(LogType.BROWSER, Level.ALL); logPrefs.enable(LogType.DRIVER, Level.ALL); firefoxOptions .setProfile(firefoxProfile) .setCapability("moz:loggingPrefs", logPrefs); return firefoxOptions; } }
ElementContainsText Class
This custom-expected condition class waits until the element contains a specific text.
//Custom Expected Condition Class public class ElementContainsText implements ExpectedCondition<Boolean> { private final String textToFind; private final By by; //Constructor (Set the given values) public ElementContainsText(final By by, final String textToFind) { this.by = by; this.textToFind = textToFind; } //Override the apply method with your own functionality @Override public Boolean apply(WebDriver webDriver) { //Find the element with given By method (By CSS, XPath, Name, etc.) WebElement element = Objects .requireNonNull(webDriver) .findElement(this.by); //Check that the element contains given text? return element .getText() .contains(this.textToFind); } //This is for log message. I override it because when test fails, it will give us a meaningful message. @Override public String toString() { return ": \"Does " + this.by + " contain " + this.textToFind + "?\""; } }
LogUtil Class
This class is for log utility for the tests.
@LazyComponent public class LogUtil { public static LogEntries getLogs(WebDriver driver) { return driver .manage() .logs() .get(LogType.BROWSER); } public void isLoginErrorLog(WebDriver driver) { //Check logs (works only Chrome and Edge) LogEntries logEntries = driver .manage() .logs() .get(LogType.BROWSER); Assert.assertTrue(logEntries .getAll() .stream() .anyMatch(logEntry -> logEntry .getMessage() .contains("An invalid email address was specified"))); } }
ScreenshotUtil Class
This utility class is responsible for taking screenshots.
@LazyComponent public class ScreenshotUtil { @Autowired private ApplicationContext ctx; @Value("${screenshot.path}") private Path path; public void takeScreenShot(String testName) throws IOException { File sourceFile = this.ctx.getBean(TakesScreenshot.class).getScreenshotAs(OutputType.FILE); FileCopyUtils.copy(sourceFile, this.path.resolve( testName + ".png").toFile()); } public byte[] getScreenshot(){ return this.ctx.getBean(TakesScreenshot.class).getScreenshotAs(OutputType.BYTES); } }
WindowSwitchUtil Class
This class is for switching windows.
@LazyComponent public class WindowSwitchUtil { @Autowired private ApplicationContext ctx; public void switchByWindowTitle(final String title) { WebDriver driver = this.ctx.getBean(WebDriver.class); driver .getWindowHandles() .stream() .map(handle -> driver .switchTo() .window(handle) .getTitle()) .filter(t -> t.startsWith(title)) .findFirst() .orElseThrow(() -> { throw new RuntimeException("There is no such window available."); }); } public void switchByIndex(final int index) { WebDriver driver = this.ctx.getBean(WebDriver.class); String[] handles = driver .getWindowHandles() .toArray(new String[0]); driver .switchTo() .window(handles[index]); } }
Now it is time to continue with test classes.
BaseTest Class
The base test class is annotated by @SeleniumTest annotation and the Lombok library’s @Getter annotation to get the instances from the base test class. We have here the common logger, application context instances, and by using the applicationContext’s WebDriver bean, we can quit the browser in the teardown() method.
@SeleniumTest @Getter public class BaseTest { protected Logger logger = LoggerFactory.getLogger(this.getClass()); @BeforeEach public void setup() { } @LazyAutowired public ApplicationContext applicationContext; @AfterEach public void teardown() { this.applicationContext .getBean(WebDriver.class) .quit(); } }
LoginTest Class
The LoginTest class extends the BaseTest class and is annotated with Junit Jupiter’s @Execution(ExecutionMode.CONCURRENT) annotation for parallel test execution. In this class, I auto-wired the LoginSteps then I used its methods to implement test scenarios for each test.
@Execution(ExecutionMode.CONCURRENT) public class LoginTest extends BaseTest { @LazyAutowired LoginSteps loginSteps; @Test public void invalidUserNameInvalidPassword() { loginSteps .givenIAmAtLoginPage() .whenILogin("onur@", "11223344") .thenIVerifyInvalidLoginMessage(); } @Test public void emptyUserEmptyPassword() { loginSteps .givenIAmAtLoginPage() .whenILogin("", "") .thenIVerifyUserNameErrorMessages("Lütfen e-posta adresinizi girin.") .thenIVerifyPasswordErrorMessage("Bu alanın doldurulması zorunludur."); } }
Cucumber BDD with Spring Boot and Selenium
I also added Cucumber BDD support to this project. You can find all details under the cucumber package. Let’s check the classes and files under this package.
Login.Feature Feature File
Login.feature is the cucumber feature file for login tests. Here we have two negative login scenarios that are annotated by @negative tag.
Feature: Login Feature @negative Scenario Outline: I login the website with invalid username and invalid password Given I am on the login page When I try to login with "<username>" and "<password>" Then I verify invalid login message Examples: | username | password | | onur@ | 11223344 | @negative Scenario Outline: I login the website with empty username and empty password Given I am on the login page When I try to login with "<username>" and "<password>" Then I verify invalid login message Examples: | username | password | | | |
LoginSteps Class
Our step definitions are in this class. Here I auto-wired the page classes and defined the steps of our scenarios.
public class LoginSteps { @Value("${browser}") private String browser; @LazyAutowired private HomePage homePage; @LazyAutowired private LoginPage loginPage; @Given("I am on the login page") public void iAmOnTheLoginPage() { homePage .goToHomePage() .goToLoginPage(); } @When("I try to login with {string} and {string}") public void iTryToLoginWithAnd(String userName, String password) { loginPage .login(userName, password); } @Then("I verify invalid login message") public void iVerifyInvalidLoginMessage() { if (!browser.equalsIgnoreCase("firefox")) { loginPage .verifyLogEntryFailMessage(); } else { loginPage.verifyPasswordErrorMessageWithCss("E-posta adresiniz veya şifreniz hatalı"); } } }
Cucumber Hooks Class
Cucumber hooks are essential to do operations before or after the steps. Here I defined these operations by using Cucumber Hooks.
public class CucumberHooks { @LazyAutowired private ScreenshotUtil screenshotUtil; @LazyAutowired private ApplicationContext applicationContext; @AfterStep public void afterStep(Scenario scenario){ if(scenario.isFailed()){ scenario.attach(this.screenshotUtil.getScreenshot(), "image/png", scenario.getName()); } } @After public void afterScenario(){ this.applicationContext.getBean(WebDriver.class).quit(); } }
CucumberSpringContextConfig Class
This class is necessary for Cucumber and Spring boot integration. I annotated this class with @CucumberContextConfiguration and @SpringBootTest annotations.
@CucumberContextConfiguration @SpringBootTest public class CucumberSpringContextConfig { }
RunCucumberTest Class
This class is for running the Cucumber tests. I used Cucumber JVM 7.x version and Junit Jupiter’s @Suite annotations. @SelectDirectories annotation is for the feature files, and @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = “com.swtestacademy.springbootselenium.cucumber”) is for gluing the steps files.
@Suite @IncludeEngines("cucumber") @SelectDirectories("src/test/java/com/swtestacademy/springbootselenium/cucumber/features") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.swtestacademy.springbootselenium.cucumber") @ConfigurationParameter(key = Constants.PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, value = "true") //@ConfigurationParameter(key = Constants.FILTER_TAGS_PROPERTY_NAME, value = "@negative") public class RunCucumberTest { }
After implementing this class, we can run the cucumber test with the command below.
mvn -Dtest="com.swtestacademy.springbootselenium.cucumber.RunCucumberTest" test
or we can use the command below as well.
mvn clean install -Dcucumber.glue="com.swtestacademy.springbootselenium.cucumber.steps" -Dcucumber.plugin="com/swtestacademy/springbootselenium/cucumber/features"
We can also add Cucumber properties to junit-platform.properties file as shown below.
cucumber.publish.enabled=true cucumber.glue=com.swtestacademy.springbootselenium.cucumber cucumber.execution.parallel.enabled=true cucumber.execution.parallel.config.strategy=dynamic cucumber.plugin=pretty, html:target/cucumber-reports/Cucumber.html, json:target/cucumber-reports/Cucumber.json, junit:target/cucumber-reports/Cucumber.xml
I will share the other properties files that I used in the project.
application.properties
The default configuration properties file is application.properties.
application.url=http://www.n11.com/ screenshot.path=/Users/onur/Desktop/temp browser=chrome logging.level.root=INFO logging.file.name=${screenshot.path}/test-execution.log default.timeout=30 #logging.pattern.file=%d %p %c{1.} [%t] %m%n #logging.pattern.console=%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
application-grid.properties
When we run the tests on Selenium Grid, I have to use application-grid.properties file as a configuration file.
selenium.grid.url=http://localhost:4444/wd/hub browser=chrome
junit-platform.properties
These are the properties of JUnit Jupiter for parallel test execution and cucumber test executions.
junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.config.strategy=dynamic cucumber.publish.enabled=true cucumber.glue=com.swtestacademy.springbootselenium.cucumber cucumber.execution.parallel.enabled=true cucumber.execution.parallel.config.strategy=dynamic cucumber.plugin=pretty, html:target/cucumber-reports/Cucumber.html, json:target/cucumber-reports/Cucumber.json, junit:target/cucumber-reports/Cucumber.xml
Spring Aspect Oriented Programming classes in the project are ElapsedTimeAspect and ScreenshotAspect classes.
ElapsedTimeAspect Class
This class is annotated with @Aspect annotation, and it uses the @Around annotation of aspectjweaver to measure the elapsed time of the methods annotated with @ElapsedTime annotation.
@Aspect @Configuration @Slf4j public class ElapsedTimeAspect { @Around("@annotation(com.swtestacademy.springbootselenium.annotations.ElapsedTime)") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object obj = proceedingJoinPoint.proceed(); long duration = System.currentTimeMillis() - startTime; log.info("Elapsed time of {} class's {} method is {}", proceedingJoinPoint .getSignature() .getDeclaringTypeName(), proceedingJoinPoint .getSignature() .getName(), duration + " ms."); return obj; } }
ScreenshotAspect Class
This class is annotated with @Aspect annotation, and it uses the @After annotation of aspectjweaver to take screenshots after the methods annotated with @TakeScreenshot annotation.
@Aspect @Component public class ScreenshotAspect { @Autowired private ScreenshotUtil screenshotUtil; @After("@annotation(takeScreenshot)") public void after(JoinPoint joinPoint, TakeScreenshot takeScreenshot) throws IOException { this.screenshotUtil.takeScreenShot(joinPoint.getSignature().getName()); } }
How to Run Tests
We can run the test in the command line with the maven command below. The below command is for the zhs terminal.
mvn -Dtest="com.swtestacademy.springbootselenium.tests.**" test
The command below is for the bash terminal.
mvn -Dtest=com.swtestacademy.springbootselenium.tests.** test
If we want to select a specific profile, we have to specify this as shown below.
mvn -Dtest="com.swtestacademy.springbootselenium.tests.**" -Dspring.profiles.active=grid test
For selenium grid execution, we should activate the selenium grid by running the Selenium Docker compose file.
docker-compose -f docker-compose-v3.yml up
# To execute this docker-compose yml file use `docker-compose -f docker-compose-v3.yml up` # Add the `-d` flag at the end for detached execution # To stop the execution, hit Ctrl+C, and then `docker-compose -f docker-compose-v3.yml down` version: "3" services: chrome: image: selenium/node-chrome:4.1.2-20220131 shm_size: 2gb depends_on: - selenium-hub environment: - SE_EVENT_BUS_HOST=selenium-hub - SE_EVENT_BUS_PUBLISH_PORT=4442 - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 - SE_NODE_MAX_SESSIONS=5 edge: image: selenium/node-edge:4.1.2-20220131 shm_size: 2gb depends_on: - selenium-hub environment: - SE_EVENT_BUS_HOST=selenium-hub - SE_EVENT_BUS_PUBLISH_PORT=4442 - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 - SE_NODE_MAX_SESSIONS=5 firefox: image: selenium/node-firefox:4.1.2-20220131 shm_size: 2gb depends_on: - selenium-hub environment: - SE_EVENT_BUS_HOST=selenium-hub - SE_EVENT_BUS_PUBLISH_PORT=4442 - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 - SE_NODE_MAX_SESSIONS=5 selenium-hub: image: selenium/hub:4.1.2-20220131 container_name: selenium-hub ports: - "4442:4442" - "4443:4443" - "4444:4444"
After this step, we can check the grid via this link: http://localhost:4444/ui/index.html#/
And then, we can run the tests via IntelliJ or maven.
On IntelliJ, you need to specify the spring profile as grid: spring.profiles.active=grid
When tests are running, you can see the status on the dashboard.
Via maven you should use the command below:
mvn -Dtest="com.swtestacademy.springbootselenium.tests.**" -Dspring.profiles.active=grid test
If you want to run the cucumber tests, you can use the command below:
mvn -Dtest="com.swtestacademy.springbootselenium.cucumber.RunCucumberTest" test
or
mvn clean install -Dcucumber.glue="com.swtestacademy.springbootselenium.cucumber.steps" -Dcucumber.plugin="com/swtestacademy/springbootselenium/cucumber/features"
On IntelliJ, you can right-click the “RunCucumberTest” class and run the tests.
That’s all for this article. It is a long yet informative and comprehensive article.
GitHub Project for Selenium Spring Boot Cucumber Test Automation Project
https://github.com/swtestacademy/selenium-springboot/tree/junit-springboot-selenium
Thanks for reading.
Onur Baskirt

Onur Baskirt is a Software Engineering Leader with international experience in world-class companies. Now, he is a Software Engineering Lead at Emirates Airlines in Dubai.
This is pretty neat!! Nice blog 👍
How is your set-up different from the usual Selenium+JUnit+Cucumber+Grid set-up?
Being relatively new to Spring boot and Selenium, I am not able to comprehend the advantage of using Spring boot for web application test automation
Running the grid part is the same. I used docker compose and brought the grid up and running. Implementation side, I used grid properties in the resource folder, created a configuration for the grid, and created beans in the run time with the help of spring boot.
Spring boot has many advantages like dependency inversion (the beans/instances are created on the runtime). You do not need to instantiate the instances classical way. You need to autowire them. Also, spring boot comes with many annotations, which I shared in the article, providing many features. If you use spring boot in test automation, you will have more artillery in your arsenal, and you can do many more with the help of these.
Does this project support TestNG? I see TestNG related dependencies added to maven. I was not able to configure for Selenium+SpringBoot+TestNG. May be you can assist
Spring Beans are not injected into the Test Class when TestNG is used, However it works fine when JUnit is used. I posted in Stackoverflow regarding this issue (was not able to post the link here )
There are courses on Udemy by Vinoth Selvaraj and Karthik K.K. They used TestNG instead of JUnit 5. I have finished both courses. I also suggest checking those. Whenever I will have time, I will add TestNG support and put that code in another branch in github.
please provide link to github
It is provided inside the article.
com.swtestacademy.springbootselenium.configuration.WebDriverConfig.java
should have below line for Chrome to work properly
WebDriverManager.chromedriver().setup();
similarly for other browsers as well in their respective methods before returning the corresponding driver;
example:
public WebDriver chromeDriver() {
WebDriverManager.chromedriver().setup();
return new ChromeDriver();
}
First, thank you very much for your comment. I have the native browser executables in my machine that’s why for these cases no need to use bonigarcia’s webdrivermanager and I faced several issues with it while I was running the tests. Please refer here: https:/install-chrome-driver-on-mac/
Hello, very details project, thank you for your work. Tell me please, when I run your example I can see colourful log evert scenario name, given, when, then and also name of class test which has all it. But in my project it is not work. Is this switchable function?
I think for that you need to install cucumber plugin to your IntelliJ IDEA.
I’m learning to take advantage of Spring Boot in my UI tests with the Selenium WebDriver API.
On that note, why does IntelliJ complain about there not being any beans of “WebDriver” type found in your BasePage class?
It should not complain because we are declaring the beans in configurations. It is working fine on my machine.
cannot get it to setup sir
hi, great blog!
what is the advantage of SimpleThreadScope if you are anyway closing the WebDriver after every test?
Doesn’t closing the WebDriver after every test make it slow?
also, maybe I am wrong, could you please give it a try?
In my opinion, if you had for e.g. 10 test cases, all of them would spin up a new chrome instance for local. this is because I guess parallel tests use fork join pool behind the scenes, so SimpleThreadScope treats them as separate threads.
You could maybe emulate this behavior by just copy-pasting what is there in Login.feature file 4-5 times over there itself.
This is for full parallelism for each atomic tests. If you run the tests in parallel it is fine imo.
Hi, my chrome version is “101.0.4951.67” but ı get error;
“org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ‘homePage’: Unsatisfied dependency expressed through field ‘driver’; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘chromeDriver’ defined in class path resource [com/swtestacademy/springbootselenium/configuration/WebDriverConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.openqa.selenium.WebDriver]: Factory method ‘chromeDriver’ threw exception; nested exception is java.lang.IllegalStateException: The path to the driver executable The path to the driver executable must be set by the webdriver.chrome.driver system property; for more information, see https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver. The latest version can be downloaded from https://chromedriver.storage.googleapis.com/index.html”
how can resolve? Thanks.
Would you try these in this article: https:/install-chrome-driver-on-mac/
Hi Onur,
How do we retry failed scenarios with this framework ?
Regards,
Quy
You can check here: https:/junit-5-how-to-repeat-failed-test/
Hi Onur,
Great guide.. have learned a lot from it.
Could you explain the scoping in bit more detail please.
Thanks
For more details, I highly suggest this course: https://www.udemy.com/course/cucumber-with-spring-boot/
Great article! I loved the ability to clone the project and get a complete, up-to-date setup for Selenium and Cucumber in a Spring setup.
I second the motion to include “WebDriverManager.chromedriver().setup();” in the code, at least as commented code. The line will help people who are not too technical or have issues downloading and using a native browser.
Also having two “steps”-directories, with two LoginSteps-classes got me confused for a bit. I prefer all steps under cucumber, but I am guessing this was just some forgotten cleanup?
Thank you so much for your comment Dag. In 2023, I will also try to update the articles and the codes. :-) When I wrote the article, I used the latest technology stack. I am happy to hear that you did not face any errors.
For some reason parallelism is not working. Even though I set to 2, it is still launching as many browser windows as the number of tests to execute. Do I have to make any changes to get it working?
Hi, maybe the reason is dynamic parallelism. In the below config properties, I suggest changing the strategy to fixed rather than dynamic.
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.config.strategy=dynamic
cucumber.publish.enabled=true
cucumber.glue=com.swtestacademy.springbootselenium.cucumber
cucumber.execution.parallel.enabled=true
cucumber.execution.parallel.config.strategy=dynamic
Details:
dynamic
Computes the desired parallelism based on the number of available processors/cores multiplied by the junit.jupiter.execution.parallel.config.dynamic.factor configuration parameter (defaults to 1).
fixed
Uses the mandatory junit.jupiter.execution.parallel.config.fixed.parallelism configuration parameter as the desired parallelism.
custom
Allows you to specify a custom ParallelExecutionConfigurationStrategy implementation via the mandatory junit.jupiter.execution.parallel.config.custom.class configuration parameter to determine the desired configuration.
Please try below configs:
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism = 2
cucumber.publish.enabled=true
cucumber.glue=com.swtestacademy.springbootselenium.cucumber
cucumber.execution.parallel.enabled=true
cucumber.execution.parallel.config.strategy=fixed
cucumber.execution.parallel.config.fixed.parallelism = 2
For me also original code is not running in parallel mode. Also, above fixed strategy configuration is not working too. Do you have any other suggestions?
Hi Satya, I have done some changes, updated libraries, and it worked well locally on my machine. Please, pull this branch and try the changes. I hope it will work for you as well. https://github.com/swtestacademy/selenium-springboot/tree/junit-springboot-selenium
Your new code is running in parallel. However I want to control how many threads it is running at any given point. As we have hundreds of tests, I don’t want to spin up hundreds of browsers to run the tests. I tried using fixed strategy with 2 threads in junit-platform.properties but it is still opening as many browsers as the number of tests. Appreciate your help.
Satya, you are right. I have seen that there are some problems with the JUnit5 default fixed execution engine with the current setup.
Link: https://github.com/SeleniumHQ/selenium/issues/10113
I have created a custom strategy (https://github.com/swtestacademy/selenium-springboot/blob/junit-springboot-selenium/src/test/java/com/swtestacademy/springbootselenium/configuration/CustomStrategy.java) and published the main branch. Please, pull the latest code and test it. It worked on my local machine.
Now, you can set the parallelism in junit-platform.properties file below way:
junit.jupiter.execution.parallel.config.custom.parallelism=2
I hope this will solve your problems. Have a good day and happy testing.
Hi Satya, Did the last solution work for you? I am curious if it is working or not. Thanks.
I want to run it locally on my machine but im facing this error instead. command i used is mvn -Dtest=”com.ztools.tests.**” test
[ERROR] invalidUserNameInvalidPassword Time elapsed: 0.598 s <<< ERROR!
java.lang.IllegalStateException: No Scope registered for scope name 'webdriverscope'
at com.ztools.tests.LoginTest.invalidUserNameInvalidPassword(LoginTest.java:18)
I have no idea how you changed the project structure. As I see, you renamed the packages. I suggest first just clone the project and run with the command which I have shared in the article. If all is ok, then step by step rename the packages like ztools etc. It seems like a configuration error.
But i have this error. I’m using chromedriver in my usr/local/bin/chromedriver. How do i set it up? It seems like it wont fire up any chromedriver nor google-chrome
unreported exception java.lang.Throwable; must be caught or declared to be thrown
Would you do the settings which are described here: https:/install-chrome-driver-on-mac/
I got it. Thanks! Sorry, im a springboot newbie.
But i have a few questions here. I have created a new service class with @Service. And I tried to call the service class, but it keeps saying that the service is null. This is how i declared the service in TestResultLoggerExtension class
@LazyAutowired
MyService myService;
[ERROR] LoginTest ? NullPointer Cannot invoke “com.zoloz.portal.utils.MyService.sendNotification(String)” because “this.myService” is null
Rather than @Service, would you try @Component or @TestComponent
Onur Baskirt : Its great article and learned lot of things
I am trying the dynamic way and want to cap the number of browser
cucumber.execution.parallel.enabled=true
cucumber.execution.parallel.config.strategy=dynamic
cucumber.execution.parallel.config.dynamic.factor=1
since I am running on 10 core machine , as per the documentation it should open 10 browsers and its opening many more 35+
can you please help me is there any issues with my config
If you don’t use Cucumber below one is for JUnit 5 only solution:
I have created a custom strategy (https://github.com/swtestacademy/selenium-springboot/blob/junit-springboot-selenium/src/test/java/com/swtestacademy/springbootselenium/configuration/CustomStrategy.java) and published the main branch. Please, pull the latest code and test it. It worked on my local machine.
Now, you can set the parallelism in junit-platform.properties file below way:
junit.jupiter.execution.parallel.config.custom.parallelism=10
I hope this will solve your problems. Have a good day and happy testing.
and for Cucumber please use below configuration in junit-platform.properties file.
cucumber.publish.enabled=true
cucumber.glue=com.swtestacademy.springbootselenium.cucumber
cucumber.execution.parallel.enabled=true
cucumber.execution.parallel.config.strategy=custom
cucumber.execution.parallel.config.custom.parallelism=10
cucumber.execution.parallel.config.custom.class=com.swtestacademy.springbootselenium.configuration.CustomStrategy
cucumber.plugin=pretty, html:target/cucumber-reports/Cucumber.html, json:target/cucumber-reports/Cucumber.json, junit:target/cucumber-reports/Cucumber.xml
and then please run the test with the below command.
mvn -Dtest=”com.swtestacademy.springbootselenium.cucumber.RunCucumberTest” test
I have tested locally by changing the “cucumber.execution.parallel.config.custom.parallelism” with 1, 2, and 3 and both worked fine as expected. I hope this solves your problem.
Hi @Rajesh,
Did these settings work for you?
I have multiple scenarios in one feature file like you have and I close/quit the browser after each scenario (using hooks like you have put). But at the beginning of second scenario, it throws Invalid Session error as it cannot find the valid driver object. Do you know how to resolve this?
Exception is as below:
org.openqa.selenium.NoSuchSessionException: invalid session id
Hi, try to get the driver object by using application context. Maybe this will help. Good luck.
this.applicationContext
.getBean(WebDriver.class)
.quit();
I’m having the same issue.
Did you manage do solve it?
Somehow it only works if the concurrent number is 2.
If it’s only 1, the second test fails.
I’ve tried to initialise the driver on the @BeforeEach of the BaseTest, but with no success.
I have just cloned the project. Then, updated the chrome driver on my machine and updated the chrome. Reference: https:/install-chrome-driver-on-mac/. After, I ran the tests by right-clicking and “RunCucumberTest” file and then clicked the run. For both settings in junit-platform.properties file as shown below, it worked as expected. I could not find any problems.
cucumber.execution.parallel.config.custom.parallelism=2
cucumber.execution.parallel.config.custom.parallelism=1
I could not reproduce your problems.
Hi Onur Baskirt
I am unable to execute scripts using grid setup I need some help how can I contact you, Please help me.
I am not giving one-to-one or one-to-many consultancy services Siva. I hope you can fix your issues. I wish you all the best. If you share your problem, the community or I will try to help if they and I have time.
Ok I am getting issue with grid execution and I need step by step procedure to run it, including docker setup for windows 10
I don’t have a Windows machine Siva. I hope someone who reads this message in the blog community will help. I can share the below link by Kaan Sariveli. Maybe this will help: https:/docker-selenium-tutorial/
ScreenshotAspect is not working, ı get error but takeScreenShot() method is not working. Can u help me? Thanks.
It was working on my machine; maybe it is because of the path you are setting. Would you please re-verify?
Hello Onur,
I just pulled from ur branch. And run RunCucumberTest class. Found out this error. Can you please advice on it?
\\\
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ‘homePage’: Unsatisfied dependency expressed through field ‘driver’: Error creating bean with name ‘chromeDriver’ defined in class path resource [com/swtestacademy/springbootselenium/configuration/WebDriverConfig.class]: Failed to instantiate [org.openqa.selenium.WebDriver]: Factory method ‘chromeDriver’ threw exception with message: org/apache/hc/client5/http/config/ConnectionConfig
Can you check this page and do those things. Looks like the code could not create the webdriver instance (bean). https:/install-chrome-driver-on-mac/
one question, how do u build testsuite like xml in testng in this project if i werent using cucumber?
Hello, Onur!
Thank you for such a detailed explanation.
For the remote execution, I would like to add the test name as a capability, but I am struggling a little bit to achieve it. I have the test name on the setup method using testInfo.getDisplayName(), but I do not known how to propagate it until the getChromeOptions method, for example.
public ChromeOptions getChromeOptions() {
ChromeOptions chromeOptions = new ChromeOptions();
LoggingPreferences logPrefs = new LoggingPreferences();
logPrefs.enable(LogType.BROWSER, Level.ALL);
logPrefs.enable(LogType.DRIVER, Level.ALL);
chromeOptions.setCapability(“goog:loggingPrefs”, logPrefs);
// this is what I want to achieve
// chromeOptions.setCapability(“name”, testInfo.getDisplayName());
return chromeOptions;
}
Do you have any idea if this is possible? Do you have any suggestions on how can I achieve it?
Thank you so much!
Hi Karla, whenever I have some time, I will check.
Hi, how to integrate this code to gitlab pipeline? Thanks.
Hi Cedric, I do not have too much information about Gitlab pipeline but you can check here: https://medium.com/swlh/automating-java-projects-with-gitlab-7d81917aeb65
Hi Onur Baskirt,
I am not using Cucumber only use TestNG, how to integrate Our framework with Extend report and send a report on mail
I suggest this article: https:/extent-reports-in-selenium-with-testng/
I do not have time to modify the whole project with TestNG but this article may help you. Good luck.
I’m currently using Spring Boot 2.7.13 and it would seem even that early there’s no need to exclude junit-vintage-engine for Spring-boot-starter-test as you still have done with SB 3 as since some 2.x version everything in the SB dependency tree uses junit 5. Does this project setup require this for some project-specific reason?
public void takeScreenShot(String testName) throws IOException {
File sourceFile = this.ctx.getBean(TakesScreenshot.class).getScreenshotAs(OutputType.FILE);
FileCopyUtils.copy(sourceFile, this.path.resolve( testName + “.png”).toFile());
}
You take TakesScreenshot from the context. I don’t see where did you put it in context?