One of the most common complaints in software teams sounds like this:
"Our automated UI tests take 45 minutes to run, and half of the failures are random flakiness."
When you dig into those slow and flaky test suites, you usually discover a pattern: the tests spend 80% of their runtime executing UI steps that aren't even the behavior being tested.
Consider this scenario: you want to test whether an Admin can delete a user.
In a traditional, naive UI test, here is what happens:
- Open the browser and navigate to the signup/creation page.
- Fill out 10 input fields to create a new user.
- Handle dropdowns, submit buttons, and wait for success banners.
- Log out.
- Log in as an Admin.
- Search for the user.
- Click Delete and confirm the dialog.
- Assert the user is removed.
Steps 1 through 4 have nothing to do with testing user deletion. If step 2 fails due to a slow dropdown animation, your test fails—even though the deletion functionality was completely fine.
Here is the mental model I use to eliminate this problem: The API + UI Hybrid Strategy.
1. The Core Rule: API for Prerequisites, UI for Behavior
The golden rule of modern test engineering is simple:
If an action is a prerequisite rather than the specific behavior under test, execute it via API.
Traditional UI Test:
[ UI Setup (30s) ] ──▶ [ UI Action (5s) ] ──▶ [ UI Assertion (2s) ]
Total: ~37 seconds (High flakiness surface)
Hybrid Test:
[ API Setup (200ms) ] ──▶ [ UI Action (5s) ] ──▶ [ UI Assertion (2s) ]
Total: ~7.2 seconds (Low flakiness surface, rock solid)
In our "Admin deletes user" example:
- API Setup: Send a POST request to
/api/v1/usersto create a fresh test user. This takes ~50ms. - UI Action: Launch the browser directly to the Admin dashboard, find the user ID, and click Delete.
- UI/API Assertion: Validate that the user is deleted in the UI, and optionally verify with a quick GET request.
You save over 30 seconds on a single test. Multiply that across a suite of 300 tests, and your regression run drops from hours to minutes.
2. Eliminating Blind Sleeps with Eventual Consistency
In microservices and distributed cloud backends, data written to one service often takes a few hundred milliseconds to propagate to search indexes or read replicas.
Many engineers try to solve this with arbitrary sleeps:
// Anti-pattern: Blind wait
Thread.sleep(5000);
Blind sleeps slow down the entire pipeline when the system is fast, and they still cause flaky failures when the network is momentarily slow.
Instead, combine API verification with bounded polling:
API Action
│
▼
Poll Backend via GET API / UI Polling
│
├──▶ State matched? ──▶ Proceed immediately
│
└──▶ Timeout reached? ──▶ Fail with clear diagnostic message
Instead of guessing how long a background task will take, verify the backend state directly through a fast query or use your framework's synchronization mechanisms.
3. Isolating Authentication in Parallel Execution
When you run tests in parallel across multiple worker threads, sharing a single static session token causes race conditions and sudden 401 Unauthorized errors.
To keep tests independent and thread-safe:
- Isolate token state per worker thread using
ThreadLocal. - Clean up the token reference after execution using
.remove().
public class AuthManager {
private static final ThreadLocal<String> tokenHolder = new ThreadLocal<>();
public static void setToken(String token) {
tokenHolder.set(token);
}
public static String getToken() {
return tokenHolder.get();
}
public static void clear() {
tokenHolder.remove(); // Prevents memory leaks and state bleeding
}
}
Each thread manages its own authentication lifecycle without colliding with other parallel runs.
4. Layering the API Framework
Just like UI frameworks need Page Objects, a scalable API framework requires clean layering:
Test / Assertion Layer (WHAT to validate)
│
▼
API Client Layer (Endpoints: UserAPI, OrderAPI, PaymentAPI)
│
▼
API Base Layer (RequestSpecification, Base URI, Auth Headers, Logging)
Clean Boundary Rules:
- API Clients own endpoints and requests:
UserAPI.createUser(payload)handles the request formatting. - Tests own assertions: Keep your validation logic (
Assert.assertEquals(...)) in the test layer, not buried inside the API client methods. - Chaining for clean flows: Build predictable pipelines:
Create → Extract ID → UI Action → Assert.
Conclusion
UI tests do not need to be painfully slow or inherently fragile.
By pushing setup and teardown into lightweight API calls and reserving the browser strictly for the critical user journeys, you transform your test suite from a slow bottleneck into a rapid, high-confidence feedback engine.