Example Java test-automation framework: API + UI + DB-over-SSH in one lean project. Distilled from a production framework — same architectural ideas.
Java 21 · Gradle (Kotlin DSL) · JUnit 5 (parallel) · Guice · Retrofit/OkHttp · Playwright · Allure · Owner (config) · Awaitility · JDBI + HikariCP · JSch (SSH tunnel) · Lombok · AssertJ
| Track | Target |
|---|---|
| API | restful-booker (or the dockerized copy on stand local) |
| UI | saucedemo.com |
| DB | Local MySQL from docker/, reachable only through an SSH tunnel via the bastion container |
| Integrated | Local Java booking API backed by the same MySQL used by DB assertions |
test ──> Steps facade (@Step, Allure) ──> Retrofit API interface / Playwright page object / JDBI DAO
│
└── wired by Guice modules, injected into tests by StepsParameterResolver
API and UI ownership is explicit at every call site: target facades expose focused domains such as
api.restfulBooker().health(), api.local().users(), and ui.sauceDemo().inventory(). Retrofit clients and
steps mirror the same target/domain package structure; see ADR 0011.
Key mechanisms (all in src/main/java/io/bookwright):
- Preconditions —
@Preconditions({BOOKING_EXISTS})on a test: thePreconditionenum holds named setup actions,PreconditionProviderruns them right before the test body (each as an Allure step) and shares created data through typedTestStoreaccessors. Every Store key is hidden beside its owning extension or storage component;NamespaceRegistrycreates scopes only. See ADR 0013. - Fixtures —
@WithAuthSessioncreates an explicit authentication value object for restful-booker. The integrated system adds@UserFixture(NEW|EXISTING): a new user is registered through the API and cleaned up automatically, while an existing user comes from Owner configuration. Tests receive one typedTestUsercontaining redacted credentials, profile, and API-issued session. Stable product scenarios use typedSauceDemoFixtures,LocalUserFixtures, andHotelDatabaseFixtures; unique payloads come from the deterministic per-testTestDatasequence. Steps accept these ready values instead of inventing scenarios; see ADR 0012. - Teardown — steps push a cleanup lambda into a per-test LIFO queue for every entity they create;
TeardownExtensiondrains it after each test.teardown.failOnErrorcontrols whether cleanup failures fail an otherwise successful test; a primary test failure is never replaced. - Extensions — auto-registered via
META-INF/services+junit-platform.properties(autodetection, parallel classes, fixed parallelism 4).UiArtifactsOnFailureExtensionattaches a screenshot, page HTML, Playwright trace, and browser diagnostics when a UI test fails. - Config — Owner interfaces with MERGE policy: system properties > env vars >
stands/${STAND}/stand.properties. Switch stands with-DSTAND=local(defaultprod). No secrets in the repo: local demo passwords are documented non-secrets, real ones come from env (DB_PASSWORD,SSH_PASSWORD). - SSH tunnel —
SshTunnel(JSch) forwards a dynamically assigned localhost port to MySQL through the bastion, opens lazily on first DB access, and closes through a run-levelTestExecutionListener. Password authentication with disabled host-key checking is restricted to the loopbacklocaldemo; non-local stands require a private key andknown_hosts. See ADR 0005. - Deterministic local stand — service images are pinned by digest and expose explicit health checks.
run-local-tests.shcreates an isolated Compose project, discovers dynamic API/SSH ports, and lets JSch reserve the tunnel port, so concurrent checkouts do not compete for fixed host ports. Configuration examples are in the infrastructure profiles guide. - Cross-layer verification — the
local-appmodule exposes a real booking API backed by the same private MySQL service.ApiDatabaseBookingTestcreates and reads through Retrofit, verifies persistence through JDBI over SSH, deletes through the API, and confirms database cleanup. The application runs only under the Composeintegratedprofile; see ADR 0009. - API-authenticated UI —
UserFixtureExtensionobtains a real application session before Playwright creates its per-test context.BrowserManagerinjects the HTTP-only session cookie, so unrelated UI scenarios open a protected page without submitting a login form. Form login remains only in tests that exercise authentication UI; see ADR 0010. - Waits — UI relies on Playwright's auto-retrying assertions; async API states are polled with
Awaitility via
Waits(shared defaults + mandatory alias, composed fluently at the call site). Examples:HealthSteps.waitUntilUp()(infrastructure warm-up),BookingSteps.waitUntilSearchableByName()(eventual consistency). The shared OkHttp client never retries implicitly; retries exist only at an explicit consistency boundary. See ADR 0004. - Reproducible test data — every test receives an isolated
TestDatasequence derived from one run seed and its JUnit identity. Parallel scheduling cannot change generated values. Allure records both seeds and an exact replay command; see ADR 0003. - Safe HTTP reporting — one interceptor produces sanitized logs and Allure attachments. Sensitive headers, query parameters, JSON fields, and form fields are redacted; unknown body formats are omitted. The rationale and trade-offs are documented in ADR 0001.
- UI failure diagnostics — every UI test gets an isolated tracing session and bounded event capture for console errors, page errors, and failed requests. Failure artifacts include the current URL and viewport; successful-test traces are discarded. See ADR 0002.
- User-facing UI locators — product actions find the card by its visible product name and resolve the button inside that card; no selector slug is derived from display text. Assertions verify complete product collections and cart → checkout → completion state transitions.
- Framework self-tests — infrastructure contracts are verified independently from product scenarios: configuration precedence, preconditions, typed user sessions, JUnit Store isolation, teardown policy, waits, deterministic data, HTTP edge cases, concurrent state/browser isolation, diagnostics, artifact isolation, and resource closure. See the verification matrix.
- Tags —
@Smoke,@Regression,@Api,@Ui,@Dbwrap JUnit@Tag;@OwnerDanilwraps Allure@Owner.
CI runs static quality, framework self-tests, API, UI, DB-over-SSH, and integrated application scenarios as independent gates. The final required status passes only when every gate succeeds. Their Allure results are then merged into one history-enabled report.
Spotless defines one repository format, JaCoCo enforces at least 60% instruction coverage for the selected framework core, Gradle verifies dependency checksums, and separate workflows run CodeQL and dependency review. Dependabot proposes grouped Gradle and GitHub Actions updates. See the CI guide for commands, scope, and recommended branch-protection checks.
# API tests (no docker needed, hits public restful-booker)
./gradlew test --tests "io.bookwright.tests.api.*"
# UI tests (Playwright downloads Chromium on first run)
./gradlew test --tests "io.bookwright.tests.ui.*"
# DB + tunnel tests on an isolated local stand (automatic cleanup)
./scripts/run-local-tests.sh --tests "io.bookwright.tests.db.*"
# API tests against the digest-pinned local restful-booker
./scripts/run-local-tests.sh --tests "io.bookwright.tests.api.*"
# local Java API -> MySQL verification, user fixtures, and API-authenticated UI
./scripts/run-local-tests.sh integrationTest
# by tags
./gradlew test -DincludeTags=smoke
./gradlew test -DincludeTags=regression -DexcludeTags=ui
# replay the exact generated data from an Allure failure
./gradlew test -Dtest.seed=4242 --tests "io.bookwright.tests.api.BookingCrudTest.bookingCanBeCreated"
# everything on the local stand + report
./scripts/run-local-tests.sh
allure serve build/allure-results
# deterministic framework checks without product systems
./gradlew qualityGate
# inspect available dependency updates
./gradlew dependencyUpdatesHeaded browser: ./gradlew test -Dui.headless=false --tests "io.bookwright.tests.ui.*"
(any config key can be overridden the same way — system properties beat the stand file).
bookwright follows Semantic Versioning. The current version has a single source of
truth in gradle.properties; ./gradlew printVersion prints it and ./gradlew validateVersion verifies it.
Release notes are maintained in CHANGELOG.md, and accepted improvements are tracked in
ROADMAP.md.
Releases are tag-driven. After updating projectVersion and moving entries from Unreleased to a dated
version in the changelog, push v<projectVersion>; CI verifies the tag, runs the full local stand, and
creates the GitHub Release from that changelog section.
Architecture decisions are recorded in docs/adr, including target/domain ownership, thin clients
and dependency injection, JUnit-owned lifecycle state, explicit cleanup, native Playwright waits, and explicit
API consistency boundaries.
src/main/java/io/bookwright/
├── annotations/ tags + owners (6 annotations, not 133)
├── api/ target/domain Retrofit interfaces + shared transport and DTOs
├── config/ Owner configs + Configs entry point
├── fixtures/ immutable target-owned product scenarios
├── db/ SshTunnel, DbPool, DAO, row mapper
├── di/ Guice modules (Api, Ui, Db)
├── junit/ extensions: preconditions, fixtures, resolver, screenshots, tunnel lifecycle
├── steps/ compact target facades + focused target/domain steps
├── teardown/ LIFO teardown queue + extension
├── ui/ BrowserManager + page objects (plain Playwright locators)
└── util/ Calls, Waits, deterministic TestData and factories
local-app/ minimal Java booking application backed by the shared MySQL
src/test/java/io/bookwright/{api,config,junit,teardown,ui,util,tests/{api,db,framework,integration,ui}}/
Senior SDET & QA Lead · Java · Playwright · Python · CI/CD Quality Gates · AI-Powered Automation
I am a QA automation engineer turned AI-first quality systems builder, with 16 years in testing and the last 7 focused on designing automation frameworks and scaling QA teams across fintech, iGaming, and e-commerce.
At B2Broker, I lead test automation for B2Core, a financial platform built on Kubernetes and microservices. I designed and built its Java automation framework from scratch with Playwright, OkHttp, and JUnit 5, helping move the team from manual regression cycles toward continuous delivery backed by automated quality checks.
My current focus is agentic quality engineering: Claude-powered workflows that generate tests from code and requirements, diagnose failures, review test quality, and connect the entire process through custom skills and MCP integrations.
I care about eliminating repetitive work through autonomous pipelines, keeping architecture clean, and treating quality as an engineering system rather than a checklist. bookwright is a public, educational expression of those principles.