Gradle + Java 21 Selenium WebDriver test automation framework using TestNG and Allure reporting.
The target application is an online calculator at http://calculator.com.
Tests run in parallel across Chrome/Firefox/Edge with automatic retry, screenshot capture, and dual reporting (Allure + Excel).
All tests follow a strict three-layer hierarchy — never skip layers:
test/ → TestNG test classes (extend BaseTest, use @Feature, @Test(description="..."))
└── steps/ → Business-level steps (annotated with @Step for Allure tracing)
└── pages/ → Selenium Page Object classes (PageFactory + @FindBy, interacts with the DOM only)
BaseTest(framework/BaseTest.java): One WebDriver instance per test class via@BeforeClass(alwaysRun = true)/@AfterClass(alwaysRun = true).@BeforeMethod(alwaysRun = true)callsBaseTestMethods.instantiateDriver()to self-heal a crashed driver before each test. Listeners are wired via@Listenersdirectly on this class.WebdriverManager(framework/manager/WebdriverManager.java):ThreadLocal<WebDriver>— essential for parallel safety. Always access the driver viaWebdriverManager.getDriver(), never pass it directly.WebdriverFactory(framework/manager/WebdriverFactory.java): Switch onconfiguration().envBrowser()(case-insensitive). Relies on Selenium Manager for automatic driver binary resolution — no manual driver files needed. Add new browsers here.BaseTestMethods(framework/BaseTestMethods.java): Sets browser to maximised and applies a 2-second implicit wait (IMPLICIT_WAIT_SECONDS = 2) after every driver creation. Do not add additional implicit waits elsewhere.
Config is managed via the Owner library reading src/test/resources/general.properties:
env.browser = Chrome # Chrome | Firefox | Edge | IE
env.url = http://calculator.com
default.webdriver.timeout = 180
env.time.zone = Europe/WarsawConfigurationManager.configuration() returns a cached singleton. System properties override the file at runtime:
.\gradlew.bat test -Denv.browser=Firefox -Denv.url=https://example.comNever hardcode config values in tests — always call configuration().
# Run all tests (also auto-generates Allure + Excel reports)
.\gradlew.bat clean test
# Run a single test class
.\gradlew.bat clean test --tests com.oleynik.gradle.selenium.example.test.CalculatorSanityTest
# Filter by TestNG groups
.\gradlew.bat clean test -Dgroups="Regression"
.\gradlew.bat clean test -Dexclude="Flaky"
# Regenerate Allure report from existing results
.\gradlew.bat allureReport
# Generate Excel report standalone (runs ReportUtils.main())
.\gradlew.bat excelReport
# Kill stale ChromeDriver processes (Windows)
taskkill /F /IM chromedriver.exe /TallureReport and excelReport are automatically triggered as finalizedBy in build.gradle — they run after every test task.
Listeners are wired via @Listeners on BaseTest — do not add them again in subclasses:
| Class | Interface | Purpose |
|---|---|---|
TestExecutionMethodListener |
IInvokedMethodListener |
Collects per-test result (timing, status, params) into TestExecutionResultCollector for Excel reporting |
ScreenshotListener |
IInvokedMethodListener |
Captures screenshot + page source on failure, attaches to Allure via @Attachment |
ResultExecutionListener |
IExecutionListener |
Writes Allure environment.properties on suite start; triggers Excel generation on suite finish |
AllureTestListener (implements TestLifecycleListener) is registered separately via Java SPI in
src/test/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener — prints test start/stop to console.
- TestNG
parallel = 'classes',threadCount = 3(configured inbuild.gradleuseTestNG {}block) TestExecutionResultCollectoruses aConcurrentLinkedQueue— thread-safe result aggregation- Gradle test-retry plugin:
maxRetries = 1,failOnPassedAfterRetry = true(a test that only passes on retry is marked as a failure)
@Feature("My feature") // Allure feature grouping — on the class
public class MyTest extends BaseTest {
@BeforeMethod(alwaysRun = true)
public void setUp() { steps = new MySteps(); }
@Test(description = "My scenario", groups = {"Regression"})
public void myTest() { ... }
}- Use
@Test(description = "...")— not a separate@Descriptionannotation. - Use
@Test(groups = {"Regression"})/@Test(groups = {"Flaky"})for tag filtering. - Always add
alwaysRun = trueto@BeforeMethodso setup runs even when groups are filtered.
- Inline data:
@DataProvider(name = "x")returningObject[][]in the same class; reference with@Test(dataProvider = "x")(seeBasicOperationsTest—additionNumbers(),subtractNumbers(),multiplyNumbers()). - Manual CSV:
@DataProviderreadsDivision.csvline-by-line viaBufferedReaderinside the test class, usingConstants.TEST_RESOURCESas the base path (seeBasicOperationsTest.divideNumbers()). - Reusable CSV:
@Test(dataProvider = "csvIntegerDataProvider", dataProviderClass = CsvDataProvider.class)+@CsvSource(path = TEST_RESOURCES + "Division.csv")—CsvDataProvideruses OpenCSV to parse the file (seeBasicDivisionTest). - Parameters are automatically captured by
TestExecutionMethodListenerviaITestResult.getParameters()and stored for Excel reporting.
Add a case to the switch in WebdriverFactory.createInstance() — that is the only place to change. Selenium Manager resolves the driver binary automatically.
The Chrome case shows the pattern for passing browser-specific options — e.g., --disable-search-engine-choice-screen suppresses the Search Engine Choice popup on Chrome:
case "chrome" -> {
ChromeOptions options = new ChromeOptions();
options.addArguments("--disable-search-engine-choice-screen");
yield new ChromeDriver(options);
}| Report | Location |
|---|---|
| Allure HTML | build/reports/allure-report/allureReport/index.html |
| TestNG HTML | build/reports/testng/ |
| Excel | build/reports/consolidatedExecutionReport_ddmmyy_HHmmss.xlsx |
| Screenshots | build/reports/screenshots/ |
| Allure raw results | build/allure-results/ |
| Excel raw results | build/excel-results/testResult_*.json (intermediate per-test JSON) |
| File | Role |
|---|---|
build.gradle |
TestNG parallel config, retry, group filtering logic, report task wiring |
framework/BaseTest.java |
Required superclass; wires all three TestNG listeners via @Listeners |
framework/config/Configuration.java |
All config keys (Owner @Config.Key) |
framework/manager/WebdriverManager.java |
ThreadLocal driver store |
framework/listeners/ScreenshotListener.java |
Failure screenshot + Allure attachment |
framework/listeners/ResultExecutionListener.java |
Suite-level reporting hook (IExecutionListener) |
framework/listeners/TestExecutionMethodListener.java |
Per-test result collector (IInvokedMethodListener) |
framework/utils/WebdriverUtils.java |
Driver lifecycle + explicit wait helpers: createNewDriver(), quitDriver(), findElement(By) (FluentWait, presenceOfElementLocated), findElement(By, Function, Integer) (custom condition + timeout), elementExists(By), elementExistsAndShown(By), clickIfElementShown(By) |
framework/config/Constants.java |
Path constants: BUILD_FOLDER, REPORTS_FOLDER, SCREENSHOTS_FOLDER, EXCEL_RESULTS_FOLDER, TEST_RESOURCES — use these whenever referencing file system paths |
src/test/resources/general.properties |
Runtime configuration |
src/test/resources/META-INF/services/ |
SPI registration for AllureTestListener only |
lombok.config |
Lombok project-level config; sets lombok.jacksonized.jacksonVersion += 2 to resolve the Jackson2/Jackson3 ambiguity warning on @Jacksonized (e.g. TestExecutionResult) |
- Strict 3-Layer Hierarchy:
pages/(DOM only, no business logic)steps/(Reusable business actions, uses@Step)test/(TestNG classes, uses@Featureand@Test(description = "..."))
- Driver Access: ALWAYS use
WebdriverManager.getDriver(). NEVER passWebDriverinstances as method arguments. - Config Access: Use
ConfigurationManager.configuration()for any property. NEVER hardcode URLs, timeouts, or browser names. Available keys:environmentUrl(),envBrowser(),defaultWebdriverTimeout(),environmentTimeZone(). - Page Objects: Use
PageFactory.initElements(WebdriverManager.getDriver(), this)in constructors. Use@FindByannotations. For conditional/dynamic locators usestatic final Byconstants and callWebdriverUtils.findElement(By)(explicit wait viaFluentWaitup todefaultWebdriverTimeoutseconds).
- Annotations:
- Test classes MUST extend
BaseTest. - Use
@Featureon class level. - Use
@Test(description = "...")for TestNG.
- Test classes MUST extend
- Data Driven:
- Prefer CSV-based data providers for complex scenarios.
- Reference files via
Constants.TEST_RESOURCES.
- Validation: Use
org.assertj.core.api.Assertionsfor expressive assertions. For soft assertions, use TestNGorg.testng.asserts.SoftAssert(as seen inCalculatorSteps.checkSoftAsserts()); AssertJSoftAssertionsis also acceptable for new code.
- Environment: Use Windows PowerShell for terminal commands (e.g.,
.\gradlew.bat). - Cleaning: Always include
cleanin test commands (.\gradlew.bat clean test) to ensure reports are fresh. - Dependencies: Before updating dependencies, check
build.gradleand synchronize versions inREADME.md.