DEV Community

Fátima Yupa
Fátima Yupa

Posted on

I Tested the Same API with Playwright, REST Assured, and Pytest: Here Is What I Found

Why I made this comparison

When we use an application, most of the work happens somewhere we cannot see. A mobile app requests user information, an online store checks its inventory, and a payment screen communicates with a banking service. APIs make all of these interactions possible.

This also means that one API failure can affect several parts of a system at once. A page may still look correct while the service behind it is returning incomplete data or accepting an invalid request. That is why API testing is such an important part of software quality.

For this article, I wanted to go beyond listing tools and their features. I used three different approaches to test the same API: Playwright with TypeScript, REST Assured with Java, and Pytest with Python Requests. Testing the same scenario made it easier to see how each option feels in practice and what type of team might benefit from it.

In every example, I checked the HTTP status, the returned JSON data, resource creation, and the response to an invalid resource. These are basic tests, but they represent the starting point of many real automation projects.

The scenario I used

Imagine a content platform that displays posts written by its users. Before releasing a new version, the team wants to know that:

  1. GET /posts/1 returns a successful response.
  2. The returned post has ID 1.
  3. The response contains a title and body.
  4. A request for a nonexistent resource does not produce a server error.
  5. A new post can be submitted with the required fields.

The examples use JSONPlaceholder:

https://jsonplaceholder.typicode.com
Enter fullscreen mode Exit fullscreen mode

JSONPlaceholder is a public API created for testing and prototyping. Its POST operation simulates the creation of a resource but does not save it permanently. This limitation is useful to keep in mind, although the same testing techniques can be used with a real e-commerce, banking, booking, or inventory service.

JSON response returned by the test API

Figure 1. JSONPlaceholder returns post 1 as a JSON object containing the user ID, post ID, title, and body.


1. Playwright with TypeScript

I first tried Playwright. Most developers know it as a browser automation tool, so its API testing support may come as a surprise. Through APIRequestContext, Playwright can send HTTP requests without opening a browser.

This was the most appealing option when I imagined a web team maintaining both interface and API tests. Instead of introducing another framework, the team can keep both types of tests in the same project and use familiar assertions and reports.

Installation

npm init playwright@latest
Enter fullscreen mode Exit fullscreen mode

API test

Create tests/posts-api.spec.ts:

import { test, expect } from '@playwright/test';

test.describe('Posts API', () => {
  test('returns post 1 with the expected structure', async ({ request }) => {
    const response = await request.get(
      'https://jsonplaceholder.typicode.com/posts/1'
    );

    await expect(response).toBeOK();

    const post = await response.json();
    expect(post.id).toBe(1);
    expect(post.userId).toBeGreaterThan(0);
    expect(post.title).toEqual(expect.any(String));
    expect(post.title.length).toBeGreaterThan(0);
    expect(post.body).toEqual(expect.any(String));
  });

  test('creates a post with valid data', async ({ request }) => {
    const payload = {
      title: "'API testing in practice',"
      body: 'A practical comparison of automation frameworks.',
      userId: 10,
    };

    const response = await request.post(
      'https://jsonplaceholder.typicode.com/posts',
      { data: payload }
    );

    expect(response.status()).toBe(201);

    const createdPost = await response.json();
    expect(createdPost).toMatchObject(payload);
    expect(createdPost.id).toBeDefined();
  });

  test('handles a nonexistent post without a server error', async ({ request }) => {
    const response = await request.get(
      'https://jsonplaceholder.typicode.com/posts/999999'
    );

    expect(response.status()).toBe(404);
    expect(response.status()).toBeLessThan(500);
  });
});
Enter fullscreen mode Exit fullscreen mode

The complete example includes successful GET and POST requests as well as a negative test for a resource that does not exist. For the practical execution shown below, I used the equivalent JavaScript syntax with require; the test behavior is the same as in the TypeScript example.

Playwright API tests written in JavaScript

Figure 2. Playwright tests validating the response structure, simulated post creation, and a 404 response.

Run the tests:

npx playwright test
Enter fullscreen mode Exit fullscreen mode

The three scenarios completed successfully during the practical execution.

Successful Playwright test execution

Figure 3. Playwright executed three API tests using one worker, and all three passed.

My impression

The code feels familiar if the project already uses TypeScript. Playwright also includes its own assertions, fixtures, parallel execution, reports, and tracing. Having those features in one place can make the initial setup easier.

However, I would probably not introduce Playwright only for a small API project. In that situation, it can feel heavier than necessary. A team that is new to JavaScript also has to become comfortable with asynchronous code and the use of async and await.


2. REST Assured with Java

REST Assured takes a different approach. It is designed specifically for REST services and uses a given–when–then structure. In simple terms, we define the request, perform an action, and describe the expected result.

I found this style very readable because the structure tells a small story. Given this configuration, when I request a resource, then I expect a particular response. It also fits naturally into Java projects that already use JUnit, Maven, Gradle, or Spring.

Maven dependencies

Add the following dependencies to pom.xml:

<dependencies>
  <dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <version>6.0.0</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>6.1.0</version>
    <scope>test</scope>
  </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

These versions were checked in Maven Central on June 24, 2026. Teams should verify them again when implementing the example because dependency versions change over time.

API test

Create PostsApiTest.java:

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;

import java.util.Map;
import org.junit.jupiter.api.Test;

class PostsApiTest {

    private static final String BASE_URL =
        "https://jsonplaceholder.typicode.com";

    @Test
    void returnsPostOneWithExpectedStructure() {
        given()
            .baseUri(BASE_URL)
        .when()
            .get("/posts/1")
        .then()
            .statusCode(200)
            .contentType("application/json; charset=utf-8")
            .body("id", equalTo(1))
            .body("userId", greaterThan(0))
            .body("title", allOf(instanceOf(String.class), not(emptyString())))
            .body("body", instanceOf(String.class));
    }

    @Test
    void createsPostWithValidData() {
        Map<String, Object> payload = Map.of(
            "title", "API testing in practice",
            "body", "A practical comparison of automation frameworks.",
            "userId", 10
        );

        given()
            .baseUri(BASE_URL)
            .contentType("application/json")
            .body(payload)
        .when()
            .post("/posts")
        .then()
            .statusCode(201)
            .body("title", equalTo(payload.get("title")))
            .body("body", equalTo(payload.get("body")))
            .body("userId", equalTo(payload.get("userId")))
            .body("id", notNullValue());
    }

    @Test
    void handlesNonexistentPostWithoutServerError() {
        given()
            .baseUri(BASE_URL)
        .when()
            .get("/posts/999999")
        .then()
            .statusCode(404)
            .statusCode(lessThan(500));
    }
}
Enter fullscreen mode Exit fullscreen mode

REST Assured tests using given, when, and then

Figure 4. REST Assured expresses the GET and POST scenarios through its readable given–when–then syntax.

Run the tests:

mvn test
Enter fullscreen mode Exit fullscreen mode

In this comparison, the REST Assured example is included to demonstrate its Java syntax. Unlike the Playwright and Pytest examples, its execution was not captured because the local environment used for the demonstration did not have Maven configured.

My impression

REST Assured was the most API-focused option in the comparison. Its syntax is expressive, and its JSON and XML assertions are powerful. For a Java or Spring team, it would be a very natural choice and would integrate well with an existing CI/CD pipeline.

The trade-off is the usual amount of Java setup and boilerplate. The example is still clear, but it needs more supporting configuration than the Python version. If the team does not work in the JVM ecosystem, there is little reason to choose it over a tool that matches the team's main language.


3. Pytest with Python Requests

Finally, I tested the API with Pytest and Requests. They are actually two separate tools: Requests sends the HTTP calls, while Pytest discovers and runs the tests. Together, they create a lightweight API testing setup.

This was the quickest version for me to read. The response behaves like a normal Python object, and the validations use ordinary assert statements. Someone with basic Python knowledge can understand the intention of the test without learning a specialized syntax first.

Installation

python -m pip install pytest requests
Enter fullscreen mode Exit fullscreen mode

API test

Create test_posts_api.py:

import requests

BASE_URL = "https://jsonplaceholder.typicode.com"
TIMEOUT = 10


def test_returns_post_one_with_expected_structure():
    response = requests.get(f"{BASE_URL}/posts/1", timeout=TIMEOUT)

    assert response.status_code == 200
    assert response.headers["Content-Type"].startswith("application/json")

    post = response.json()
    assert post["id"] == 1
    assert post["userId"] > 0
    assert isinstance(post["title"], str)
    assert post["title"]
    assert isinstance(post["body"], str)


def test_creates_post_with_valid_data():
    payload = {
        "title": "API testing in practice",
        "body": "A practical comparison of automation frameworks.",
        "userId": 10,
    }

    response = requests.post(
        f"{BASE_URL}/posts",
        json=payload,
        timeout=TIMEOUT,
    )

    assert response.status_code == 201

    created_post = response.json()
    assert created_post["title"] == payload["title"]
    assert created_post["body"] == payload["body"]
    assert created_post["userId"] == payload["userId"]
    assert "id" in created_post


def test_handles_nonexistent_post_without_server_error():
    response = requests.get(
        f"{BASE_URL}/posts/999999",
        timeout=TIMEOUT,
    )

    assert response.status_code == 404
    assert response.status_code < 500
Enter fullscreen mode Exit fullscreen mode

The first part of the Python file covers the successful retrieval and creation scenarios.

Pytest code for GET and POST requests

Figure 5. Pytest and Requests use standard Python assertions to validate GET and POST responses.

The final test checks the negative case separately:

Pytest negative test for a nonexistent resource

Figure 6. A request for post 999999 verifies that the API returns status code 404.

Run the tests:

pytest -v
Enter fullscreen mode Exit fullscreen mode

All three Python tests passed in the practical execution.

Successful Pytest execution

Figure 7. Pytest collected and successfully completed the three API scenarios.

My impression

The strongest point is simplicity. Python also has a large ecosystem for generating test data, working with databases, validating schemas, and creating reports. Pytest fixtures and parameterization provide a good path for growing the project beyond a few examples.

That flexibility can also become a weakness. Because the solution is assembled from separate libraries, advanced features may require extra plugins and decisions. A larger suite needs a clear structure; otherwise, URLs, request logic, and assertions can quickly become duplicated across many files.


Framework comparison

Criterion Playwright REST Assured Pytest + Requests
Main language TypeScript/JavaScript Java Python
Initial learning curve Medium Medium Low
API-only projects Good Excellent Excellent
Combined UI and API tests Excellent Limited Requires other tools
Readability High High BDD-style syntax Very high
Built-in test runner Yes Uses JUnit/TestNG Pytest
CI/CD integration Excellent Excellent Excellent
Best fit Web teams using TypeScript Enterprise/JVM systems Python teams and rapid automation

So, which one would I choose?

After writing the three versions, I do not think there is a universal winner. The HTTP requests and assertions are similar; the real difference is how naturally each framework fits into a team's daily work.

  • Choose Playwright when the same team needs browser and API automation in a TypeScript project.
  • Choose REST Assured when the system and development ecosystem are based on Java or Spring.
  • Choose Pytest with Requests when simplicity, fast development, and Python integration are priorities.

Personally, I found Pytest the easiest to read, but that does not automatically make it the best professional choice. If I were already working on a TypeScript application with Playwright browser tests, I would keep the API tests there. In a Java and Spring organization, REST Assured would make much more sense.

Language is only one part of the decision. Reporting, authentication, schema validation, test data, parallel execution, maintainability, and CI/CD integration matter just as much once the test suite begins to grow.

Lessons that matter beyond the framework

One detail became especially clear during this comparison: checking only status code 200 is not enough. A server can return 200 with incorrect or incomplete data. A useful test must also inspect the response body and confirm that the business behavior is correct.

In a real project, I would follow these practices:

  1. Test successful and unsuccessful requests.
  2. Validate response data, headers, and schemas.
  3. Add timeouts so tests do not wait forever.
  4. Keep URLs, credentials, and tokens in environment variables.
  5. Avoid using production customer data.
  6. Generate independent test data and clean it after execution.
  7. Never assume that tests will run in a specific order.
  8. Run a fast smoke suite on every change and a broader suite on a schedule.
  9. Publish reports so failures are visible to the whole team.
  10. Treat test code with the same quality standards as application code.

Final thoughts

Comparing these frameworks with the same scenario helped me see that API testing is not mainly about finding the “best” syntax. Playwright, REST Assured, and Pytest are all capable of creating reliable automated tests. What changes is the context in which each one feels most comfortable.

Playwright stands out when UI and API automation need to live together. REST Assured feels at home in a Java ecosystem and provides the most specialized API syntax. Pytest with Requests is direct, readable, and easy to adapt, especially for teams that already use Python.

My main conclusion is simple: the framework should support the team instead of forcing the team to change the way it works. More importantly, no tool creates quality by itself. Quality comes from choosing meaningful scenarios, checking the returned data, and testing what happens when users or systems send something unexpected.

References

  1. Playwright. “API Testing.” https://playwright.dev/docs/api-testing
  2. Playwright. “APIResponseAssertions.” https://playwright.dev/docs/api/class-apiresponseassertions
  3. REST Assured. Official website and examples. https://rest-assured.io/
  4. REST Assured. “Usage Guide.” https://github.com/rest-assured/rest-assured/wiki/usage
  5. Maven Central. “REST Assured 6.0.0.” https://central.sonatype.com/artifact/io.rest-assured/rest-assured
  6. Maven Central. “JUnit Jupiter 6.1.0.” https://central.sonatype.com/artifact/org.junit.jupiter/junit-jupiter
  7. Pytest. Official documentation. https://docs.pytest.org/
  8. Requests. Official documentation. https://requests.readthedocs.io/
  9. Postman. “Run and test collections from the command line using Newman CLI.” https://learning.postman.com/docs/reference/newman-cli/command-line-integration-with-newman/
  10. Aldaine, Alice. “Top 10 API Testing Tools in 2020.” Medium. https://alicealdaine.medium.com/top-10-api-testing-tools-rest-soap-services-5395cb03cfa9

Suggested partner comment

Abstract

This article compares Playwright, REST Assured, and Pytest with Requests by implementing the same REST API test scenario in TypeScript, Java, and Python. The comparison demonstrates that all three alternatives can validate status codes, JSON content, resource creation, and negative responses. However, their main advantages depend on the development ecosystem: Playwright is especially effective for combined UI and API automation, REST Assured is a natural choice for Java enterprise projects, and Pytest offers concise and flexible Python tests. The article also emphasizes that framework selection is less important than testing meaningful business behavior and maintaining reliable, independent test cases.

Important observation

A particularly important point is the inclusion of negative testing. Many beginner API suites verify only successful 200 responses, but real systems also need predictable behavior for invalid identifiers, missing authentication, malformed payloads, and insufficient permissions. A strong extension to this comparison would be adding JSON Schema or OpenAPI contract validation to each framework.

Top comments (0)