Layout
src/main/java/pages/ LoginPage, ProductsPage...
src/main/java/utils/ ConfigReader, DriverFactory
src/test/java/tests/ LoginTest, api/...
src/test/resources/ config.properties, testdata/
Config + override
# config.properties
baseUrl=https://www.saucedemo.com
headless=false
// ConfigReader.get(key):
return System.getProperty(key, props.getProperty(key));
mvn test "-DbaseUrl=https://staging.example.com"
DriverFactory (headless)
ChromeOptions o = new ChromeOptions();
if (Boolean.parseBoolean(ConfigReader.get("headless"))) {
o.addArguments("--headless=new");
o.addArguments("--window-size=1920,1080"); // ALWAYS
}
return new ChromeDriver(o);
CI workflow (.github/workflows/tests.yml)
on: [push] + workflow_dispatch + schedule (cron)
steps: checkout@v4 -> setup-java@v4 (temurin 21)
-> mvn -B test -Dheadless=true
upload-artifact@v4 if: failure()
path: screenshots/ + target/surefire-reports/
Parallel (junit-platform.properties)
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent
# classes parallel, methods sequential = safe default
cron, decoded
"0 2 * * *" = every day at 02:00 UTC
| | | | |
| | | | +-- day of week (0 = Sunday)
| | | +---- month
| | +------ day of month
| +--------- hour
+------------ minute
Secrets & the badge
# repo Settings -> Secrets and variables -> Actions
env:
APP_PASSWORD: ${{ secrets.APP_PASSWORD }}
# never commit credentials — read them from env in CI

Only-fails-in rules
- Only in CI → headless window size, first suspect
- Only in parallel → shared state; hunt the static
- Only sometimes → missing explicit wait