Build Faster and More Reliable E2E Automation
UI automation is useful for validating real user journeys, but creating test data entirely through the UI can make tests slow and difficult to maintain.
For example, imagine testing this flow:
Login ↓Create User ↓Search User ↓Open User Details ↓Verify User Information
If we create the user through the UI, the test may need to perform several additional steps:
Login ↓Navigate to Users ↓Click Add User ↓Enter Name ↓Enter Email ↓Enter Address ↓Save ↓Search User ↓Verify User
Instead, we can use an API to create the user and then use Playwright UI automation to verify that user.
API → Create Test Data ↓ Playwright UI ↓ Verify in UI
This approach makes UI tests faster, more independent, and easier to maintain.
In this article, we will build this approach using Playwright + Java.
1. Project Setup
For this example, we will use:
- Java
- Playwright
- Maven
- JUnit 5
- REST API
- Page Object Model
A simple project structure could look like this:
playwright-api-ui/│├── src│ ├── test│ │ └── java│ │ ├── api│ │ │ └── UserApi.java│ │ ││ │ ├── pages│ │ │ ├── LoginPage.java│ │ │ └── UsersPage.java│ │ ││ │ └── tests│ │ └── UserUITest.java│ ││ └── test│ └── resources│└── pom.xml
2. Add Playwright and JUnit Dependencies
Add the following dependencies to pom.xml:
<dependencies> <dependency> <groupId>com.microsoft.playwright</groupId> <artifactId>playwright</artifactId> <version>1.55.0</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.13.4</version> <scope>test</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.20.0</version> </dependency></dependencies>
Use the versions that are current in your project rather than blindly copying these version numbers.
You might like to READ: API Testing with Playwright
3. Create Test Data Through an API
Let’s assume our application provides this endpoint:
POST /api/users
Request:
{ "name": "John Automation", "email": "john123@test.com", "role": "QA"}
Response:
{ "id": 101, "name": "John Automation", "email": "john123@test.com", "role": "QA"}
Instead of creating this user through the UI, we can create it through the API.
4. Playwright APIRequestContext
One useful feature of Playwright is that API testing can be performed using Playwright’s APIRequestContext.
Create a class:
package api;import com.microsoft.playwright.APIRequest;import com.microsoft.playwright.APIRequestContext;import com.microsoft.playwright.APIResponse;import com.microsoft.playwright.Playwright;public class UserApi { private final APIRequestContext request; public UserApi(Playwright playwright) { request = playwright.request().newContext( new APIRequest.NewContextOptions() .setBaseURL("https://example.com") .setExtraHTTPHeaders( java.util.Map.of( "Content-Type", "application/json" ) ) ); } public String createUser(String name, String email) { String requestBody = """ { "name": "%s", "email": "%s", "role": "QA" } """.formatted(name, email); APIResponse response = request.post( "/api/users", APIRequestContext.PostOptions.builder() .setData(requestBody) .build() ); System.out.println("Status Code: " + response.status()); System.out.println("Response: " + response.text()); return response.text(); } public void close() { request.dispose(); }}
Now we can create test data without opening the browser.
5. Extract the User ID from the API Response
Often the API response contains an ID that we need later.
For example:
{ "id": 101, "name": "John Automation", "email": "john123@test.com"}
We can use Jackson to extract the ID.
ObjectMapper objectMapper = new ObjectMapper();JsonNode responseJson = objectMapper.readTree(response.text());int userId = responseJson.get("id").asInt();System.out.println("Created User ID: " + userId);
Our API method can therefore return the ID instead of the entire response.
public int createUser(String name, String email) { String requestBody = """ { "name": "%s", "email": "%s", "role": "QA" } """.formatted(name, email); APIResponse response = request.post( "/api/users", APIRequestContext.PostOptions.builder() .setData(requestBody) .build() ); if (response.status() != 201) { throw new RuntimeException( "User creation failed. Status: " + response.status() ); } JsonNode json = objectMapper.readTree(response.text()); return json.get("id").asInt();}
Now the test can use:
int userId = userApi.createUser( "John Automation", "john123@test.com");
6. Start Playwright Browser
Now let’s move to the UI.
Playwright playwright = Playwright.create();Browser browser = playwright.chromium().launch( new BrowserType.LaunchOptions() .setHeadless(false));BrowserContext context = browser.newContext();Page page = context.newPage();
Navigate to the application:
page.navigate("https://example.com");
7. Create a Login Page Object
A simple Page Object:
package pages;import com.microsoft.playwright.Page;public class LoginPage { private final Page page; public LoginPage(Page page) { this.page = page; } public void login(String username, String password) { page.locator("#username") .fill(username); page.locator("#password") .fill(password); page.locator("#login") .click(); }}
8. Create a Users Page Object
Suppose the UI displays users like this:
Users--------------------------------John Automationjohn123@test.comQA--------------------------------
Create:
package pages;import com.microsoft.playwright.Page;public class UsersPage { private final Page page; public UsersPage(Page page) { this.page = page; } public void searchUser(String email) { page.locator("#user-search") .fill(email); } public boolean isUserDisplayed(String email) { return page .locator("text=" + email) .isVisible(); }}
9. Combine API and UI Testing
Now comes the important part.
The API creates the test data.
The UI verifies the test data.
@Testvoid verifyUserCreatedThroughAPIIsDisplayedInUI() { int userId = userApi.createUser( "John Automation", "john123@test.com" ); System.out.println( "Created user ID: " + userId ); LoginPage loginPage = new LoginPage(page); UsersPage usersPage = new UsersPage(page); loginPage.login("admin", "password"); usersPage.searchUser("john123@test.com"); assertTrue( usersPage.isUserDisplayed( "john123@test.com" ) );}
This is the core idea:
API
│
▼
Create Test User
│
▼
Get User ID/Email
│
▼
Open Application
│
▼
Login
│
▼
Search User
│
▼
Verify User
10. Complete Example
Let’s combine everything into a realistic test class.
package tests;import api.UserApi;import com.microsoft.playwright.*;import org.junit.jupiter.api.*;import pages.LoginPage;import pages.UsersPage;import static org.junit.jupiter.api.Assertions.assertTrue;public class UserUITest { static Playwright playwright; static Browser browser; BrowserContext context; Page page; UserApi userApi; @BeforeAll static void setupAll() { playwright = Playwright.create(); browser = playwright.chromium().launch( new BrowserType.LaunchOptions() .setHeadless(false) ); } @BeforeEach void setup() { context = browser.newContext(); page = context.newPage(); userApi = new UserApi(playwright); } @Test void verifyUserCreatedThroughAPI() { String email = "john" + System.currentTimeMillis() + "@test.com"; int userId = userApi.createUser( "John Automation", email ); System.out.println( "Created User ID: " + userId ); page.navigate( "https://example.com/login" ); LoginPage loginPage = new LoginPage(page); UsersPage usersPage = new UsersPage(page); loginPage.login( "admin", "password" ); usersPage.searchUser(email); assertTrue( usersPage.isUserDisplayed(email), "Created user is not displayed" ); } @AfterEach void tearDown() { userApi.close(); context.close(); } @AfterAll static void tearDownAll() { browser.close(); playwright.close(); }}
11. Why Use API for Test Data?
Consider a test that requires five users.
UI approach
Open applicationLoginNavigate to UsersCreate User 1Create User 2Create User 3Create User 4Create User 5Run actual test
This introduces unnecessary UI operations.
API approach
userApi.createUser( "User1", "user1@test.com");userApi.createUser( "User2", "user2@test.com");userApi.createUser( "User3", "user3@test.com");
Then:
Create test data through API ↓Open UI ↓Run actual test scenario
The UI test focuses on what the user actually does, rather than spending time creating prerequisites.
12. API Login Instead of UI Login
We can take this approach further.
Suppose the application uses a login API:
POST /api/login
Request:
{ "username": "admin", "password": "password"}
Response:
{ "token": "eyJhbGciOi..."}
We can retrieve the token using the API.
APIResponse response = request.post( "/api/login", APIRequestContext.PostOptions.builder() .setData(""" { "username": "admin", "password": "password" }
13. Using Storage State
Playwright supports saving authenticated browser state.
For example:
context.storageState( new BrowserContext.StorageStateOptions() .setPath( java.nio.file.Paths.get( "auth.json" ) ));
Later, create a browser context using that state:
BrowserContext context = browser.newContext( new Browser.NewContextOptions() .setStorageStatePath( java.nio.file.Paths.get( "auth.json" ) ) );
Now tests can start with an already authenticated session when the application’s authentication model supports this approach.
14. API Setup in @BeforeEach
Another useful pattern is preparing test data before every test.
@BeforeEachvoid setupTestData() { email = "user" + System.currentTimeMillis() + "@test.com"; userId = userApi.createUser( "Automation User", email );}
Then the actual test becomes very clean:
@Testvoid verifyUserInUI() { page.navigate( "https://example.com/users" ); usersPage.searchUser(email); assertTrue( usersPage.isUserDisplayed(email) );}
The test itself contains only the business scenario.
15. API Cleanup After the Test
Test data should ideally be cleaned up.
For example:
public void deleteUser(int userId) { APIResponse response = request.delete( "/api/users/" + userId ); if (response.status() != 204) { throw new RuntimeException( "Failed to delete user: " + userId ); }}
Then:
@AfterEachvoid cleanup() { userApi.deleteUser(userId); context.close();}
The lifecycle becomes:
@BeforeEach │ ▼Create User through API │ ▼Run UI Test │ ▼Delete User through API │ ▼@AfterEach
This makes tests much more isolated.
16. API + UI Testing for Different Scenarios
This strategy isn’t limited to creating users.
Scenario 1 — Create Order
POST /orders ↓Create order ↓Open UI ↓Search order ↓Verify order
Scenario 2 — Create Product
POST /products ↓Create product ↓Open UI ↓Search product ↓Verify product
Scenario 3 — Create Customer
POST /customers ↓Create customer ↓Open UI ↓Search customer ↓Verify customer
Scenario 4 — Update Backend Data
PUT /users/101 ↓Change user status ↓Open UI ↓Verify status
Scenario 5 — Delete Data
DELETE /users/101 ↓Open UI ↓Verify user no longer exists
17. Keep API and UI Responsibilities Separate
A good framework should not put everything inside the test class.
Avoid this:
@Testvoid testUser() { // API request // JSON parsing // Browser setup // Login // UI locators // Assertions // API cleanup}
Instead, separate responsibilities:
tests/ UserUITest.javaapi/ UserApi.java LoginApi.javapages/ LoginPage.java UsersPage.javamodels/ User.java
The test should read almost like a business scenario:
@Testvoid verifyUser() { User user = userApi.createUser( "John", "john@test.com" ); loginPage.login( "admin", "password" ); usersPage.searchUser( user.getEmail() ); usersPage.verifyUser( user.getEmail() );}
This is considerably easier to maintain.
18. Using a POJO for Test Data
Instead of passing multiple parameters, create a User class.
public class User { private int id; private String name; private String email; private String role; public User() { } public User( String name, String email, String role ) { this.name = name; this.email = email; this.role = role; } public int getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } public String getRole() { return role; }}
Then:
User user = userApi.createUser( new User( "John Automation", "john@test.com", "QA" ) );
This becomes particularly useful when your API response contains many fields.
19. API Assertions Before Starting UI Testing
Don’t blindly continue to the UI if test-data creation failed.
For example:
APIResponse response = userApi.createUserResponse( "John", email );assertEquals( 201, response.status(), "User creation failed");
Only after successful API setup should the UI test continue.
API Request │ ├── 201 → Continue UI test │ └── 4xx/5xx → Fail test immediately
This makes failures much easier to diagnose.
20. API + UI Testing with Parallel Execution
Playwright works well with parallel test execution because each test can use its own BrowserContext.
For example:
BrowserContext context = browser.newContext();Page page = context.newPage();
Each test should ideally have isolated:
- Browser context
- Page
- Test data
- API data
- Authentication state
For example:
Test 1 ├── API User A ├── Context A └── Page ATest 2 ├── API User B ├── Context B └── Page BTest 3 ├── API User C ├── Context C └── Page C
Avoid sharing a single Page object between parallel tests.
Bad:
static Page page;
Better:
BrowserContext context;Page page;
created independently for each test.
21. A Practical Parallel Test Pattern
With JUnit 5, parallel execution can be enabled through configuration.
Create:
src/test/resources/└── junit-platform.properties
Add:
junit.jupiter.execution.parallel.enabled=truejunit.jupiter.execution.parallel.mode.default=concurrentjunit.jupiter.execution.parallel.mode.classes.default=concurrent
Now tests can execute concurrently, provided your framework is designed for isolation.
For example:
@Testvoid verifyUser() { // User API setup // User UI validation}@Testvoid verifyOrder() { // Order API setup // Order UI validation}
The key requirement is test isolation, not simply turning on parallel execution.
22. A Better Framework Architecture
For a larger automation framework, I would structure it like this:
src/test/java│├── api│ ├── UserApi.java│ ├── OrderApi.java│ └── LoginApi.java│├── pages│ ├── LoginPage.java│ ├── UsersPage.java│ └── OrdersPage.java│├── models│ ├── User.java│ └── Order.java│├── tests│ ├── UserTest.java│ └── OrderTest.java│├── utils│ ├── ConfigReader.java│ └── TestDataUtil.java│└── base └── BaseTest.java
The execution flow becomes:
Test
│
┌───────┴────────┐
│ │
API Layer UI Layer
│ │
Create Data Page Objects
│ │
└───────┬────────┘
│
Assertions
23. When Should You Use API vs UI?
A simple rule is:
Use API for setup
Create usersCreate productsCreate ordersCreate configurationReset test dataAuthenticate
Use UI for user journeys
Click buttonsFill formsNavigate pagesVerify displayed dataValidate UI behaviorCheck error messages
For example:
// APIUser user = userApi.createUser( "John", "john@test.com" );// UIusersPage.searchUser( user.getEmail());usersPage.verifyUser( user.getEmail());
The API handles the setup.
The UI handles the behavior being tested.
24. Common Mistakes to Avoid
Mistake 1: Creating all test data through the UI
This makes tests unnecessarily slow.
Prefer:
userApi.createUser(...);
over:
usersPage.clickAddUser();usersPage.enterName(...);usersPage.enterEmail(...);usersPage.clickSave();
when the purpose of the test is not to test user creation itself.
Mistake 2: Sharing test data between tests
Avoid:
Test 1 → User ATest 2 → User ATest 3 → User A
Prefer:
Test 1 → User ATest 2 → User BTest 3 → User C
This becomes especially important during parallel execution.
Mistake 3: Ignoring API failures
Don’t do this:
userApi.createUser(...);page.navigate(...);
without validating the API response.
Instead:
APIResponse response = userApi.createUserResponse(...);assertEquals(201, response.status());
Mistake 4: Putting API code inside Page Objects
A Page Object should primarily represent UI behavior.
Avoid:
UsersPage.createUserThroughAPI();
Prefer:
UserApi.createUser();
and:
UsersPage.verifyUser();
25. The Most Useful Pattern
For most Playwright + Java automation frameworks, this pattern is extremely useful:
TEST
│
▼
┌─────────────────┐
│ API SETUP │
│ │
│ Create User │
│ Create Order │
│ Prepare Data │
└────────┬────────┘
│
▼
┌─────────────────┐
│ PLAYWRIGHT │
│ UI │
│ │
│ Login │
│ Navigate │
│ Search │
│ Verify │
└────────┬────────┘
│
▼
┌─────────────────┐
│ API CLEANUP │
│ │
│ Delete User │
│ Delete Order │
└─────────────────┘
This gives you a clean separation between test-data management and UI behavior validation.
26. Final Takeaway
API and UI testing should not necessarily be treated as two completely separate automation worlds.
With Playwright Java, you can use APIs to efficiently:
- Create test data
- Update test data
- Delete test data
- Authenticate users
- Prepare application state
and then use Playwright UI automation to validate the actual user experience.
The most important pattern is:
// Arrange through APIUser user = userApi.createUser( "John Automation", "john@test.com" );// Act through UIloginPage.login( "admin", "password");usersPage.searchUser( user.getEmail());// Assert through UIusersPage.verifyUser( user.getEmail());
Instead of making the UI automation perform every setup step, let the API handle the setup and let Playwright focus on the UI behavior that actually needs to be tested.
This approach can make a Playwright + Java framework significantly faster, more maintainable, and better suited for parallel execution.
Related Links
Playwright Architecture Explained
Playwright Setup with Java for Beginners
Playwright Auto-Waiting Mechanism
Playwright UI Elements Interactions
Playwright Parallel Testing (Java)
Playwright Page Object Model Example
Playwright CI/CD Integration Guide
Playwright Cloud Execution: BrowserStack vs TestMu AI
Building a Scalable Playwright Java UI Test Automation Framework
Playwright Interview Questions (Beginner to Advanced)
Discover more from Rotebit
Subscribe to get the latest posts sent to your email.
