Skip to content

Repository files navigation

bookwright

Java 21 Gradle JUnit 5 Playwright Retrofit Allure GitHub Release Quality Gates CodeQL Allure Report

Example Java test-automation framework: API + UI + DB-over-SSH in one lean project. Distilled from a production framework — same architectural ideas.

Stack

Java 21 · Gradle (Kotlin DSL) · JUnit 5 (parallel) · Guice · Retrofit/OkHttp · Playwright · Allure · Owner (config) · Awaitility · JDBI + HikariCP · JSch (SSH tunnel) · Lombok · AssertJ

What it tests

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

Architecture

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: the Precondition enum holds named setup actions, PreconditionProvider runs them right before the test body (each as an Allure step) and shares created data through typed TestStore accessors. Every Store key is hidden beside its owning extension or storage component; NamespaceRegistry creates scopes only. See ADR 0013.
  • Fixtures@WithAuthSession creates 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 typed TestUser containing redacted credentials, profile, and API-issued session. Stable product scenarios use typed SauceDemoFixtures, LocalUserFixtures, and HotelDatabaseFixtures; unique payloads come from the deterministic per-test TestData sequence. 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; TeardownExtension drains it after each test. teardown.failOnError controls 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). UiArtifactsOnFailureExtension attaches 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 (default prod). No secrets in the repo: local demo passwords are documented non-secrets, real ones come from env (DB_PASSWORD, SSH_PASSWORD).
  • SSH tunnelSshTunnel (JSch) forwards a dynamically assigned localhost port to MySQL through the bastion, opens lazily on first DB access, and closes through a run-level TestExecutionListener. Password authentication with disabled host-key checking is restricted to the loopback local demo; non-local stands require a private key and known_hosts. See ADR 0005.
  • Deterministic local stand — service images are pinned by digest and expose explicit health checks. run-local-tests.sh creates 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-app module exposes a real booking API backed by the same private MySQL service. ApiDatabaseBookingTest creates and reads through Retrofit, verifies persistence through JDBI over SSH, deletes through the API, and confirms database cleanup. The application runs only under the Compose integrated profile; see ADR 0009.
  • API-authenticated UIUserFixtureExtension obtains a real application session before Playwright creates its per-test context. BrowserManager injects 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 TestData sequence 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, @Db wrap JUnit @Tag; @OwnerDanil wraps Allure @Owner.

Quality gates

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.

Running

# 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 dependencyUpdates

Headed 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).

Versioning

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.

Layout

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}}/

Author

Danil Trofimov

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.

LinkedIn · GitHub

About

Production-grade example of a Java test automation framework: API (Retrofit) + UI (Playwright) + DB over SSH tunnel. Preconditions, LIFO teardowns, JUnit 5 extensions, Guice, Owner configs, Allure.

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages