API and UI Testing in Playwright Java

API and UI Testing with Playwright Java

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.



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 Automation
john123@test.com
QA
--------------------------------

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.

@Test
void 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 application
Login
Navigate to Users
Create User 1
Create User 2
Create User 3
Create User 4
Create User 5
Run 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.

@BeforeEach
void setupTestData() {
email =
"user" + System.currentTimeMillis()
+ "@test.com";
userId =
userApi.createUser(
"Automation User",
email
);
}

Then the actual test becomes very clean:

@Test
void 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:

@AfterEach
void 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:

@Test
void testUser() {
// API request
// JSON parsing
// Browser setup
// Login
// UI locators
// Assertions
// API cleanup
}

Instead, separate responsibilities:

tests/
UserUITest.java
api/
UserApi.java
LoginApi.java
pages/
LoginPage.java
UsersPage.java
models/
User.java

The test should read almost like a business scenario:

@Test
void 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 A
Test 2
├── API User B
├── Context B
└── Page B
Test 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=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent

Now tests can execute concurrently, provided your framework is designed for isolation.

For example:

@Test
void verifyUser() {
// User API setup
// User UI validation
}
@Test
void 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 users
Create products
Create orders
Create configuration
Reset test data
Authenticate

Use UI for user journeys

Click buttons
Fill forms
Navigate pages
Verify displayed data
Validate UI behavior
Check error messages

For example:

// API
User user =
userApi.createUser(
"John",
"john@test.com"
);
// UI
usersPage.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 A
Test 2 → User A
Test 3 → User A

Prefer:

Test 1 → User A
Test 2 → User B
Test 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 API
User user =
userApi.createUser(
"John Automation",
"john@test.com"
);
// Act through UI
loginPage.login(
"admin",
"password"
);
usersPage.searchUser(
user.getEmail()
);
// Assert through UI
usersPage.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

What is Playwright?

Playwright Architecture Explained

Playwright Setup with Java for Beginners

Playwright Locator Strategies

Playwright Auto-Waiting Mechanism

Playwright UI Elements Interactions

Playwright Parallel Testing (Java)

Playwright Page Object Model Example

API Testing with Playwright

Debugging & Trace Viewer

Playwright CI/CD Integration Guide

Playwright Cloud Execution: BrowserStack vs TestMu AI

Selenium vs Playwright

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.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply