Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# CLAUDE.md

This file provides guidance to AI assistants working in the **`framework/` module**. For the
whole mega-repo (all seven pillars + toolchains), see the root [`../CLAUDE.md`](../CLAUDE.md).

## Project

Java 21 / Maven test automation framework covering Web (Selenium 4), API (Rest Assured), Mobile (Appium 2.x), and Performance (Gatling). TestNG is the runner; Allure is the reporter.

Package root: `ra.hul.framework` (production code in `src/main/java`) and `ra.hul.tests` (test classes in `src/test/java`). This split is enforced — framework infrastructure must never live under `src/test`.

## Common commands

```bash
# Build & verify
mvn clean compile
mvn test-compile

# Run by module (each profile points Surefire at a different TestNG suite XML)
mvn test -Pweb
mvn test -Papi
mvn test -Pmobile
mvn test -Psmoke # cross-module, group="smoke"
mvn test # all-tests.xml

# Maturity capabilities (each profile swaps the suite XML)
mvn test -Pvisual -Dheadless=true -Dbrowser=chrome # visual regression (needs a browser)
mvn test -Pa11y -Dheadless=true -Dbrowser=chrome # accessibility scan (needs a browser)
mvn test -Pcontract # Pact contract + datafaker tests (no browser)

# Single test class / method (Surefire still uses the suite XML, so -Dtest is filtering)
mvn test -Pweb -Dtest=LoginTest
mvn test -Pweb -Dtest=LoginTest#login_validCredentials_shouldShowSecurePage

# Environment + browser overrides
mvn test -Pweb -Denv=stage -Dbrowser=firefox -Dheadless=false
mvn test -Pweb -Dgrid.url=http://localhost:4444 # switches to RemoteWebDriver

# Performance
mvn gatling:test
mvn gatling:test -Dgatling.simulationClass=ra.hul.framework.performance.simulations.HttpBinGetSimulation

# Reporting
mvn allure:serve # generates + opens report
mvn allure:report # writes to target/site/allure-maven-plugin
```

Mobile tests require an emulator + running Appium server before `mvn test -Pmobile`. Full setup steps are in `MOBILE_SETUP.md` — `ANDROID_HOME` must be exported in the same shell that runs Maven.

## Architecture — what's non-obvious

**4-level config resolution (`ConfigManager`).** Precedence, highest first: OS env var (dot.key → `DOT_KEY`) → `-D` system property → `config-<env>.properties` (loaded when `-Denv=<env>` is set) → `config.properties`. Missing keys throw `IllegalStateException` — use `getOrDefault` if absence is legal. Any new tunable belongs in `config.properties` and read through this manager; never hardcode timeouts, URLs, or paths in tests.

**ThreadLocal driver isolation.** `DriverManager` (web) and `AppiumDriverManager` (mobile) each hold a `ThreadLocal<...Driver>`. Base test classes (`BaseWebTest`, `BaseMobileTest`) initialize per `@BeforeMethod` and quit per `@AfterMethod`. Parallel execution is safe because of this, *not* because of any locking. **Do not** introduce static driver fields or share drivers across threads.

**Parallelism is owned by TestNG suite XMLs, not Maven.** `web-tests.xml` runs methods in 10 threads; `api-tests.xml` runs methods in 20; `mobile-tests.xml` runs classes in 1. To change parallelism, edit the suite XML or override `parallel.count` via `-D`. Surefire's fork settings are not used for this.

**Sealed `BrowserStrategy`.** Java 21 sealed interface restricts implementations to `ChromeStrategy`, `FirefoxStrategy`, `EdgeStrategy` at compile time. `WebDriverFactory` selects a strategy based on the `browser` config and decides local vs. `RemoteWebDriver` based on whether `grid.url` is set. Adding a new browser means a new sealed permits entry plus a strategy class — there is no runtime registry.

**Allure `@Step` requires AspectJ weaver.** The Surefire `argLine` in `pom.xml` injects `-javaagent:.../aspectjweaver.jar`. If `@Step` annotations stop appearing in reports, that javaagent is the first thing to check. Don't remove the `argLine` block when editing other Surefire config.

**Surefire is pinned to the TestNG provider.** The Pact JVM `junit5` artifacts pull JUnit 5 onto the test classpath, which makes Surefire auto-select the JUnit Platform provider and silently ignore our TestNG suite XMLs (`Tests run: 0`). The Surefire plugin therefore declares a `surefire-testng` plugin-level dependency to force the TestNG provider. Do not remove it while the Pact deps are present.

**Auto-applied retry.** `RetryTransformer` is a TestNG `IAnnotationTransformer` registered in every suite XML — it attaches `RetryAnalyzer` (count from `retry.count`) to every `@Test` automatically. Individual tests do not declare retry. To disable for a specific test, the right move is to make `RetryAnalyzer` honor an opt-out attribute, not to special-case the transformer.

**Page/Screen Object Model is enforced, not optional.**
- Tests must not reference Selenium `By` or Appium locators directly.
- Page objects (`web/pages/`) and screen objects (`mobile/screens/`) own all locators as `private final` fields.
- Public methods on page/screen objects are annotated `@Step` for Allure traceability.
- Assertions live in test classes, never inside page/screen objects.
- `BasePage.isLoaded()` and `BaseScreen.isLoaded()` are template-method hooks — every concrete page/screen implements one.

**No `Thread.sleep`.** Use `WaitUtils` (web) or `MobileWaitUtils` (mobile). Both wrap explicit/fluent waits with config-driven timeouts from `TimeoutConstants`.

## Test naming convention

`methodUnderTest_condition_expectedBehavior` — e.g. `login_validCredentials_shouldShowSecurePage`. Enforced by convention; new tests should match.

Every test method declares `@Epic`, `@Feature`, `@Story`, `@Severity` (Allure metadata) and `groups = {"regression"}` at minimum. Critical happy paths additionally tag `"smoke"` to be picked up by `smoke-tests.xml`.

## Adding new tests

- New web test: add a page object under `ra.hul.framework.web.pages` extending `BasePage`, then a test class under `ra.hul.tests.web` extending `BaseWebTest`. Register the test class in `web-tests.xml` (and `smoke-tests.xml` if applicable).
- New API test: extend `BaseApiTest`. POJOs go under `ra.hul.framework.api.models` with Lombok `@Data @Builder @NoArgsConstructor @AllArgsConstructor`. JSON schemas for contract tests go in `src/test/resources/schemas/`.
- New mobile test: add a screen object under `ra.hul.framework.mobile.screens` extending `BaseScreen`. Prefer `AppiumBy.accessibilityId()`. Test class extends `BaseMobileTest` and is registered in `mobile-tests.xml`.

A test class that is not added to its suite XML will silently not run.

## Maturity capabilities

Four self-contained, fully offline capabilities (no cloud/SaaS). All tunables are read via
`ConfigManager.getOrDefault`/`getIntOrDefault`/`getLongOrDefault` so absence never crashes; defaults
and keys live in `src/test/resources/config.properties` (this module has no `src/main/resources`, so
config is loaded from the test classpath).

- **Visual regression** — `web/utils/VisualRegressionUtils` (`src/main`). Homegrown per-pixel
`BufferedImage` diff, no external visual SaaS. Captures via `DriverManager.getDriver()` +
`TakesScreenshot` (reuses the AllureTestListener screenshot idiom), compares against a committed
baseline under `visual.baseline.dir` (default `src/test/resources/visual/baseline/`), writes
actual+diff to `visual.output.dir` (default `target/visual/`), attaches baseline/actual/diff to
Allure. Never asserts — returns `VisualComparisonResult`; the test asserts. Keys:
`visual.baseline.dir`, `visual.output.dir`, `visual.pixel.tolerance`, `visual.diff.threshold`,
`visual.update.baselines` (set `true` to (re)write baselines instead of failing). Tests:
`tests/visual/VisualRegressionTest`, suite `visual-tests.xml`, profile `visual`.
- **Accessibility** — `web/utils/AccessibilityUtils` (`src/main`) wraps axe-core's `AxeBuilder`,
filters by `a11y.tags` (default `wcag2a,wcag2aa`), attaches violations (JSON via `AxeReporter`
+ readable summary) to Allure, returns `List<Rule>` for the test to assert. Because a `src/main`
util references axe, the `com.deque.html.axe-core:selenium` dep is **compile** scope (not test).
Tests: `tests/a11y/AccessibilityTest`, suite `a11y-tests.xml`, profile `a11y`.
- **Contract testing** — `tests/contract/` (test-only). Real Pact JVM consumer test using the DSL
**programmatically** (`ConsumerPactBuilder` + `runConsumerTest`), NOT the JUnit5 extension. Uses
the V3 model (`RequestResponsePact`) end-to-end so the embedded-`HttpServer` provider verification
can replay `RequestResponseInteraction`s. Pact files land in `pact.output.dir` (default
`target/pacts/`). Suite `contract-tests.xml` (also runs `DataFactoryTest`), profile `contract`.
- **Test-data management** — `data/` (`src/main`): `UserFactory`, `PostPayloadFactory`,
`CredentialFactory` + `Credentials` value object, backed by datafaker via `FakerProvider`
(seeded from `data.faker.seed` / `data.faker.locale` → deterministic). Fluent `withX(...)`
overrides win over generated values; `build()` generates all fields in a fixed order so faker
consumption stays deterministic regardless of overrides. `datafaker` is **compile** scope (used
from `src/main`). Do NOT route the web login creds through a factory — `tomsmith`/
`SuperSecretPassword!` must match the live demo site. Tests: `tests/data/DataFactoryTest`.

## CI

`.github/workflows/test-automation.yml` runs web + api jobs in parallel on push to main/develop and on PRs, then merges Allure results and deploys the report to `gh-pages` with 20-run history. Mobile + performance are `workflow_dispatch` only.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

A production-grade test automation framework built with Java 21, covering Web, API, Mobile, and Performance testing. Designed for real-world adoption and structured around industry-standard design patterns.

> **Part of the [SDET Interview Prep mega-repo](../README.md).** This is the Java automation pillar; its
> TypeScript counterpart is [`../playwright/`](../playwright/). See also [`../dsa/`](../dsa/) (coding),
> [`../sdet/`](../sdet/) (practical problems + company bank), [`../sd/`](../sd/) (system design), and
> [`../study-tracker/`](../study-tracker/) (spaced-repetition tracker).

---

## Table of Contents
Expand Down Expand Up @@ -177,6 +182,38 @@ mvn test -Psmoke
mvn test
```

### Maturity Capabilities

Four higher-maturity capabilities are wired in, each fully self-contained and offline (no cloud
accounts or SaaS). Each has its own profile that swaps the TestNG suite XML.

```bash
# Visual regression (homegrown pixel-diff) -- needs a browser
mvn test -Pvisual -Dheadless=true -Dbrowser=chrome

# Accessibility scan (axe-core) -- needs a browser
mvn test -Pa11y -Dheadless=true -Dbrowser=chrome

# Contract testing (Pact JVM) + test-data factories -- no browser needed
mvn test -Pcontract
```

| Capability | What it does | Key classes | Config keys |
|-----------|--------------|-------------|-------------|
| **Visual regression** | Captures a page/element screenshot, pixel-diffs it against a committed baseline PNG with a configurable tolerance/threshold, writes a highlighted diff, attaches baseline/actual/diff to Allure. Set `-Dvisual.update.baselines=true` to refresh baselines instead of failing. | `web/utils/VisualRegressionUtils` | `visual.baseline.dir`, `visual.output.dir`, `visual.pixel.tolerance`, `visual.diff.threshold`, `visual.update.baselines` |
| **Accessibility** | Runs an [axe-core](https://github.com/dequelabs/axe-core) WCAG scan of the current page/subtree, filters by WCAG tags, attaches violations (JSON + readable summary) to Allure. | `web/utils/AccessibilityUtils` | `a11y.tags`, `a11y.fail.on.violation` |
| **Contract testing** | Real Pact JVM **consumer** test built with the DSL programmatically (TestNG-friendly, no JUnit5 runner): spins up the Pact mock server, drives `ApiClient` at it, writes the pact to `target/pacts/`. Plus a lightweight embedded-`HttpServer` **provider** verification that replays the pact. | `tests/contract/ConsumerContractTest`, `tests/contract/ProviderContractVerificationTest` | `pact.output.dir` |
| **Test-data management** | Deterministic [datafaker](https://www.datafaker.net/)-backed factories (seeded from config) with per-field overrides. | `data/UserFactory`, `data/PostPayloadFactory`, `data/CredentialFactory`, `data/Credentials`, `data/FakerProvider` | `data.faker.seed`, `data.faker.locale` |

**Baselines** live under `src/test/resources/visual/baseline/` (committed). On a fresh checkout the
first visual run generates the baseline and passes; subsequent runs compare against it. Actual/diff
artifacts are written to `target/visual/`. Bundled deterministic sample pages live under
`src/test/resources/pages/` (`visual-sample.html`, `visual-sample-modified.html`, `a11y-sample.html`).

> **Note:** because the Pact JVM `junit5` artifacts drag JUnit 5 onto the test classpath, the
> Surefire plugin pins the **TestNG** provider (`surefire-testng` plugin dependency) so our TestNG
> suite XMLs are still honoured. Do not remove that pin.

### Environment Selection

```bash
Expand Down
63 changes: 63 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
<jackson.version>3.1.0</jackson.version>
<lombok.version>1.18.44</lombok.version>

<!-- Maturity Capability Versions -->
<axe-selenium.version>4.10.1</axe-selenium.version>
<datafaker.version>2.4.3</datafaker.version>
<pact.version>4.6.17</pact.version>

<!-- Plugin Versions -->
<maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.5.5</maven-surefire-plugin.version>
Expand Down Expand Up @@ -129,6 +134,34 @@
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>

<!-- Test-data generation (datafaker) — used by src/main data factories, so compile scope -->
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>${datafaker.version}</version>
</dependency>

<!-- Accessibility (axe-core Selenium binding) — referenced by the src/main AccessibilityUtils, so compile scope -->
<dependency>
<groupId>com.deque.html.axe-core</groupId>
<artifactId>selenium</artifactId>
<version>${axe-selenium.version}</version>
</dependency>

<!-- Contract testing (Pact JVM) — test only; DSL used programmatically (no JUnit5 runner) -->
<dependency>
<groupId>au.com.dius.pact.consumer</groupId>
<artifactId>junit5</artifactId>
<version>${pact.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>au.com.dius.pact.provider</groupId>
<artifactId>junit5</artifactId>
<version>${pact.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -166,6 +199,18 @@
<allure.results.directory>${project.build.directory}/allure-results</allure.results.directory>
</systemPropertyVariables>
</configuration>
<!--
Pin the TestNG provider. The Pact JVM junit5 artifacts drag JUnit 5 onto the
test classpath, which would otherwise make Surefire auto-select the JUnit
Platform provider and silently ignore our TestNG suite XMLs (Tests run: 0).
-->
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-testng</artifactId>
<version>${maven-surefire-plugin.version}</version>
</dependency>
</dependencies>
</plugin>

<!-- Gatling -->
Expand Down Expand Up @@ -217,5 +262,23 @@
<suiteXmlFile>src/test/resources/smoke-tests.xml</suiteXmlFile>
</properties>
</profile>
<profile>
<id>visual</id>
<properties>
<suiteXmlFile>src/test/resources/visual-tests.xml</suiteXmlFile>
</properties>
</profile>
<profile>
<id>a11y</id>
<properties>
<suiteXmlFile>src/test/resources/a11y-tests.xml</suiteXmlFile>
</properties>
</profile>
<profile>
<id>contract</id>
<properties>
<suiteXmlFile>src/test/resources/contract-tests.xml</suiteXmlFile>
</properties>
</profile>
</profiles>
</project>
64 changes: 64 additions & 0 deletions src/main/java/ra/hul/framework/data/CredentialFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package ra.hul.framework.data;

import net.datafaker.Faker;

/**
* Fluent factory that builds {@link Credentials} test data backed by a seeded {@link Faker}.
* Deterministic under a fixed {@code data.faker.seed}; overrides win over generated values.
*
* <p>NOTE: these are synthetic credentials for API/data-driven tests. Do not use them for the
* live web login demo (that requires the real {@code tomsmith}/{@code SuperSecretPassword!}).</p>
*
* <pre>{@code
* Credentials c = CredentialFactory.newCredentials().withUsername("qa_bot").build();
* }</pre>
*/
public final class CredentialFactory {

private final Faker faker;

private String username;
private String password;
private String email;

private CredentialFactory(Faker faker) {
this.faker = faker;
}

/** Factory seeded from config ({@code data.faker.seed} / {@code data.faker.locale}). */
public static CredentialFactory newCredentials() {
return new CredentialFactory(FakerProvider.seeded());
}

/** Factory backed by a caller-supplied Faker (e.g. a specific seed). */
public static CredentialFactory newCredentials(Faker faker) {
return new CredentialFactory(faker);
}

public CredentialFactory withUsername(String username) {
this.username = username;
return this;
}

public CredentialFactory withPassword(String password) {
this.password = password;
return this;
}

public CredentialFactory withEmail(String email) {
this.email = email;
return this;
}

public Credentials build() {
String genUsername = faker.internet().username();
String genPassword = faker.internet().password(10, 16, true);
String genEmail = faker.internet().emailAddress();

return Credentials.builder()
.username(username != null ? username : genUsername)
.password(password != null ? password : genPassword)
.email(email != null ? email : genEmail)
.build();
}
}
20 changes: 20 additions & 0 deletions src/main/java/ra/hul/framework/data/Credentials.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ra.hul.framework.data;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* Immutable-ish value object holding a generated set of login credentials.
* Not tied to any real account — produced by {@link CredentialFactory} for test data.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Credentials {
private String username;
private String password;
private String email;
}
Loading
Loading