Halmurat T.
Halmurat T.

Senior SDET

Home Blog Books ask About

The Dispatch

Weekly QA notes from the trenches.

Welcome aboard!

You're on the list. Expect real-world QA insights — no fluff, no spam.

© 2026 Halmurat T.

Automation 24
  • Selenium
  • Playwright
  • Appium
  • Cypress
AI Testing 5
CI/CD 6
  • GitHub Actions
  • Slack Reporting
QA Strategy 4
Case Studies 5
Blog/Case Studies
Case StudiesHalmurat T./April 9, 2026/11 min

We Cut 150 Min of Test Setup with 3 Java Classes

Filed undercase-study/api-testing/playwright/framework-design/enterprise
We Cut 150 Min of Test Setup with 3 Java Classes

Table of Contents
  • The Application Nobody Writes Tutorials About
  • How HAR Files Became the Blueprint
  • Should You Use page.request() or Standalone APIRequestContext?
  • The 3-Class Solution
  • Class 1: ApiClient — The HTTP Foundation
  • Class 2: AccountApiService — The Domain Wrapper
  • Class 3: Account POJO — The Data Factory
  • How Do You Handle Cleanup When Tests Fail?
  • Results
  • What I’d Do Differently

On this page

  • The Application Nobody Writes Tutorials About
  • How HAR Files Became the Blueprint
  • Should You Use page.request() or Standalone APIRequestContext?
  • The 3-Class Solution
  • Class 1: ApiClient — The HTTP Foundation
  • Class 2: AccountApiService — The Domain Wrapper
  • Class 3: Account POJO — The Data Factory
  • How Do You Handle Cleanup When Tests Fail?
  • Results
  • What I’d Do Differently

200 of our 250 Playwright tests spent more time creating test data than actually testing. Each test clicked through 4-5 forms — search an account, load it, copy it, fill out policy details — before a single assertion ran. The suite burned roughly 150 minutes on data setup alone. Three Java classes — an API client, a domain service, and a data factory — brought that under 10.

The Application Nobody Writes Tutorials About

The platform was a large insurance underwriting system. Every screen lived inside nested iframes. Authentication was session-based — JSESSIONID cookies from a form login, no Bearer tokens. And the “API” was a 15-year-old JSONServlet that spoke form-action protocol instead of REST.

Every Playwright API tutorial out there shows the same thing: POST /api/users with a clean JSON body. Our reality looked like this:

actual-api-request-payload.json
[
{ "action": "pm_saveProgram",
"viewId": "programEditSummaryView",
"actionClass": "com.example.ProgramAction" },
{ "programForm": {
"clientId": "7033",
"uwId": "205",
"underwritingYear": "2026",
"policyStartDate": "01/01/2026"
}
}
]

An array where the first element defines the server action and the second carries form data. Responses came back with globalMessage, cascading selects for dropdowns, and server-generated IDs buried three levels deep. Not exactly the { "id": 42 } you see in documentation examples.

How HAR Files Became the Blueprint

I recorded HAR files for every critical flow — create, search, copy, delete. Not to mock anything at runtime, but to understand what the API actually expected. When you’re staring at a JSONServlet that accepts arrays of form-action payloads, the official documentation (if it exists) is usually wrong or outdated. The HAR file is the truth.

Here’s what the recordings revealed:

  1. The protocol is stateful. Changing the underwriter field triggers a server-side cascade that reloads available business classes. The response depends on prior submissions in the same session.
  2. IDs are environment-specific. Client ID 7033 maps to “Entergy Corporation” in QA3 but a different client in QA1.
  3. Form data is order-dependent. The JSONServlet validates that you’ve called initView before pm_saveProgram. Calling operations out of sequence returns validation errors.

Once I understood the protocol from the HAR recordings, wrapping each operation in a service method was straightforward.

[ TIP ]

Record HAR files for every flow you want to automate. They’re your Rosetta Stone — the most accurate documentation of what the API actually expects, especially for legacy systems where formal API docs don’t exist.

Should You Use page.request() or Standalone APIRequestContext?

Use page.request() when your app relies on session-based auth. Use standalone APIRequestContext when you need API calls before the browser launches or with separate credentials.

page.request() returns an APIRequestContext that shares cookies with the browser context automatically. Authenticate via API and the browser gets the session. Log in through the browser and API calls carry the cookie. For our JSESSIONID-based app, this was non-negotiable.

Standalone contexts from playwright.request().newContext() have isolated cookie jars — useful for admin operations with different credentials, but they won’t share state with your UI tests.

[ WARNING ]

In Playwright Java, APIResponse has no json() method — unlike Node.js and Python. You must call response.text() and deserialize with Jackson or Gson. This catches people off guard when porting examples from the Node.js docs.

The 3-Class Solution

Each class has one job. Together, they replace 150 minutes of UI clicking.

Class 1: ApiClient — The HTTP Foundation

Wraps page.request() for cookie sharing, handles serialization with Jackson, and provides clean error logging:

src/main/java/core/network/ApiClient.java
public class ApiClient {
private final APIRequestContext request;
private final String baseUrl;
private final ObjectMapper mapper = new ObjectMapper();
public ApiClient(Page page, String baseUrl) {
this.request = page.request();
this.baseUrl = baseUrl;
}
public ApiResponse post(String path, Object body) {
APIResponse raw = request.post(
baseUrl + path,
RequestOptions.create().setData(body));
return new ApiResponse(raw.status(), raw.text());
}
}

Line 4: Jackson handles the JSON serialization the framework doesn’t give us natively. Line 7: page.request() means every call carries the browser’s session cookie — no token management required.

Class 2: AccountApiService — The Domain Wrapper

This is where the JSONServlet complexity gets buried. Every public method maps to a user-visible operation — create, search, copy, delete — while hiding the array-based protocol underneath:

src/test/java/framework/AccountApiService.java
public class AccountApiService {
private static final String SERVLET = "oldApi/JSONServlet";
private final ApiClient client;
public AccountCreateResult createAccount(Account account) {
Object[] payload = new Object[]{
Map.of("action", "pm_saveProgram",
"viewId", "programEditSummaryView"),
Map.of("programForm", account.toFormData())
};
ApiResponse response = client.post(SERVLET, payload);
return parseAccountId(response);
}
public void deleteAccount(int accountId) {
Object[] payload = new Object[]{
Map.of("action", "pm_removeProgram",
"viewId", "programEditSummaryView"),
Map.of("programForm", Map.of("accountId", accountId))
};
client.post(SERVLET, payload);
}
}

Lines 6-9: That array protocol — first element for the action, second for form data — is the pattern every JSONServlet call follows. Once you recognize it, wrapping new operations takes minutes.

The service also maintains lookup maps for environment-specific IDs:

src/test/java/framework/AccountApiService.java
private static final Map<String, String> CLIENT_MAP = Map.of(
"ENTERGY CORPORATION", "7033",
"ACME INSURANCE GROUP", "4521"
);

Hardcoded? Yes. Fragile? Somewhat. But these client-to-ID mappings change once a year at most, and maintaining them beats clicking through a search dialog 200 times per suite run.

Class 3: Account POJO — The Data Factory

Factory methods generate valid test data with sensible defaults and unique names for parallel test isolation:

src/test/java/pages/pojos/Account.java
public static Account getRandomPropertyAccount() {
Account account = new Account();
account.setAccountName("H_Auto_" + RandomTestData.getSimpleDate()
+ "_" + RandomTestData.getRandomNumeric(5));
account.setUnderwriter("John Smith");
account.setInsuranceType("Property");
account.setApiClientId(CLIENT_MAP.get("ENTERGY CORPORATION"));
account.setPolicyStartDate(RandomTestData.getTodayDate());
account.setPolicyEndDate(RandomTestData.getTomorrowDate());
return account;
}

Lines 3-4: The name prefix H_Auto_ plus a timestamp and random number guarantees uniqueness across parallel workers. It also enables pattern-based bulk cleanup — more on that next.

How Do You Handle Cleanup When Tests Fail?

Orphaned test data is the silent cost of API-driven setup. A test creates an account, fails mid-assertion, and the @AfterMethod cleanup never executes cleanly. After a few failed runs, the environment is littered with stale test accounts.

We used two patterns together:

Pattern 1: Bulk delete by name prefix with a safety limit to prevent accidental mass deletion:

src/test/java/framework/AccountApiService.java
public int deleteAccounts(String namePattern, int safetyLimit) {
List<AccountRef> matches = searchAccounts(namePattern);
if (matches.size() > safetyLimit) {
throw new IllegalStateException(
"Found " + matches.size() + " matches for '"
+ namePattern + "' — exceeds safety limit of "
+ safetyLimit);
}
matches.forEach(ref -> deleteAccount(ref.getId()));
return matches.size();
}

Line 4: That safety limit exists because I once wrote a cleanup query with a pattern that matched production-like account names. In a QA environment, no harm done. In UAT? That’s a career-limiting bug. The safety limit forces you to be deliberate.

Pattern 2: @AfterMethod cleanup with the name prefix:

src/test/java/tests/BaseTestDI.java
@AfterMethod(alwaysRun = true)
public void cleanupTestData() {
new AccountApiService(apiClient())
.deleteAccounts("H_Auto_", 50);
}

The alwaysRun = true ensures cleanup runs even when the test fails. The pattern H_Auto_ catches everything our factory creates. The safety limit of 50 protects against surprises.

Results

~150 min

Setup time (before)

~10 min

Setup time (after)

200 of 250

Tests affected

3

Classes added

The numbers break down simply. Each UI-based test data setup — searching an account, loading it, copying it through form clicks — took roughly 45 seconds. Through the API, the same operation completes in about 3 seconds. Across 200 tests, that’s 150 minutes down to 10.

Beyond speed, we gained reliability. UI-based setup was the #1 source of flaky tests caused by session cookie corruption. Slow-loading iframes, intermittent form validation popups, and stale element references during setup — all gone. Tests now test the feature they’re supposed to test, not the data creation flow.

Parallel execution also improved. UI-based setup with shared browser state caused thread safety issues we’d been patching for months. API setup is inherently thread-safe — each call is stateless and independent.

What I’d Do Differently

Start with the registry pattern from day one. We added the bulk-delete-by-prefix approach as a workaround after orphaned data became a problem. A proper registry that tracks every created entity and cleans up in reverse order would have been cleaner.

Use standalone APIRequestContext for admin operations. Some cleanup tasks need admin credentials that differ from the test user’s session. Mixing admin and user operations through the same page.request() means the browser’s cookie jar gets overwritten. Separate contexts for separate roles.

Extract the lookup maps to config. Hardcoded client-to-ID maps work until you add a new environment. A properties file per environment — loaded by the existing ConfigReaderService — would have saved us from editing Java code every time QA4 came online.

§ Frequently Asked FAQ
+ Does page.request() share cookies with the browser?

Yes. page.request() returns an APIRequestContext that automatically shares cookies with the BrowserContext. API authentication flows set cookies the browser uses, and browser logins set cookies API calls carry. This is the key reason to prefer page.request() over standalone contexts for session-based apps.

+ How should I use HAR files in my test automation?

Use HAR files as documentation. Record the manual flow to understand exactly what the API expects — endpoints, payloads, headers, response formats. Then wrap those exact calls in service methods. For legacy APIs like JSONServlet, the HAR recording is often more accurate than any written documentation. It’s your most reliable reference for building the API layer.

+ How do I prevent orphaned test data in parallel tests?

Use unique name prefixes per test (timestamp + random number), run cleanup in @AfterMethod with alwaysRun = true, and add a safety limit to bulk delete operations. The name prefix pattern lets you clean up by pattern match without needing individual account IDs.

+ Does this pattern work with Playwright Node.js or Python?

Yes — the architecture (ApiClient, domain service, data factory) is language-agnostic. In Node.js you get a json() method on APIResponse which simplifies deserialization. The pattern of wrapping legacy APIs in a clean service layer applies to any language Playwright supports.

+ Why not use direct database inserts instead of API calls?

Database inserts bypass business logic and validation. Our JSONServlet enforces field dependencies, cascading lookups, and audit logging that direct SQL would skip. API-driven setup creates data that matches what a real user would produce — with all the server-side side effects intact. If you need to validate that your API contracts are intact, you need to go through the API.

This API data layer is one pillar of a controlled test environment — the broader strategy for isolating your test suite from external instability.

Start with your HAR files. Record the manual flow you want to automate, read the protocol, then wrap those exact API calls in a service class with a clean interface. You don’t need a modern REST API to do API-driven test data management — you just need the discipline to hide the ugliness behind a good abstraction.

§ Further Reading 03 of 03
01Case Studies

Our Enterprise Approved AI — And Why It's the Biggest Risk

Enterprises lock teams into outdated AI models for safety. The irony? Older, less capable models produce worse code and create more risk than they prevent.

Read →
02Case Studies

The Flaky Test Isn't Flaky — It's a Race Condition

How a 'flaky' Playwright test exposed a shared test user race condition in our parallel suite, and the isolation patterns that fixed it for good.

Read →
03Case Studies

Shared Session Cookies Corrupted Our Parallel Tests

Eight Playwright BrowserContexts were properly isolated, but they all loaded the same session cookie — so the server treated 8 parallel threads as one.

Read →

Don't miss a thing

Subscribe to get updates straight to your inbox.

HT

No spam · Unsubscribe anytime

Welcome aboard!

You're on the list. Expect real-world QA insights — no fluff, no spam.

§ Colophon

Halmurat T. — Senior SDET writing about test automation, CI/CD, and QA strategy from 10+ years in the enterprise trenches.

Set in
IBM Plex Sans, Lora, and IBM Plex Mono.
Built with
Astro, MDX, Tailwind CSS & Expressive Code. Served by Vercel.
Privacy
No cookies. No tracking scripts on the main thread — analytics run sandboxed via Partytown.
Source
github.com/Halmurat-Uyghur
Terminal
Try /ask to query Halmurat's notes in a shell prompt.

© 2026 Halmurat T. · Written in plain text, shipped in plain time.

Search
Esc

Search is not available in dev mode.

Run npm run build then npm run preview:local to test search locally.