Python API POST Test | Verify Data Creation with pytest & requests

test-automation

📌 Who This Article Is For

  • Those who want to write POST request tests with Python and requests
  • QA engineers who want to learn how to verify data creation in API testing
  • Those who want to implement positive, validation, and negative POST tests
  • Those who want to automate API testing with pytest for their portfolio

What You Will Learn

  • The basic pattern for writing POST request API tests with pytest and requests
  • How to verify responses after data creation (status, body, headers)
  • How to implement negative tests for validation errors
  • How to write production-level test code using raise_for_status()

👨‍💻 About the Author

Working as a QA engineer handling API test automation with Python, pytest, and requests in real-world projects. All code used in this article is publicly available on GitHub and has been verified to work as described. View code on GitHub →

When writing API tests with Python and pytest, POST request testing comes right after GET. Using pytest and requests, we can automatically verify “Is the data being created correctly?” and “Are validation errors returned properly?”

This article covers everything from the basics of POST requests to negative testing and validation verification with production-ready code.


00. API Testing and the Basics of POST Requests

A POST request is an operation that “creates new data.” Unlike GET, it sends data in the request body to the server.

In POST testing, we mainly verify the following.

Verification ItemContentExample
Status CodeDoes 201 return on successful creation?201 Created
Response BodyIs the sent data returned correctly?name and email values
ID AssignmentIs the newly created resource assigned an ID?“id”: 11
ValidationDoes sending invalid data return an appropriate error?400 Bad Request

💡 Key Takeaway:POST testing has more verification points than GET. Always test not just positive cases but also negative cases with validation errors to ensure API quality.


01. API Test Environment with pytest + requests

If you haven’t set up your environment yet, install the required libraries first.

pip install requests pytest pytest-html

We’ll use the free mock API JSONPlaceholder as our test target.

POST https://jsonplaceholder.typicode.com/users

Here is a sample request body.

new_user = {
    "name": "Yoshitsugu Tester",
    "username": "yoshitsugu728",
    "email": "yoshitsugu@example.com",
    "phone": "090-1234-5678",
    "website": "qa-auto-lab.com"
}

💡 Key Takeaway:JSONPlaceholder is a mock API so data is not actually saved. However, the response format is the same as a real API, making it ideal for learning API testing.


02. How to Write API POST Tests in Python

Basic POST Test (201 Verification)

Send a POST request and confirm that 201 Created is returned.

import requests

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

def test_post_user_status_code():
    """TC01: Status code 201 should be returned on data creation"""
    new_user = {
        "name": "Yoshitsugu Tester",
        "username": "yoshitsugu728",
        "email": "yoshitsugu@example.com"
    }
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    assert response.status_code == 201, \
        f"Expected: 201, Got: {response.status_code}"

    print(f"\n✅ TC01 PASS | Status code: {response.status_code}")

Detecting 4xx/5xx with raise_for_status()

def test_post_user_raise_for_status():
    """TC02: Exception should be raised on 4xx/5xx errors"""
    new_user = {"name": "Test User", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    # Automatically raises HTTPError on 4xx/5xx
    response.raise_for_status()

    assert response.status_code == 201
    print(f"\n✅ TC02 PASS | raise_for_status() passed normally")

💡 Key Takeaway:response.raise_for_status() is an essential one-liner in production. Always add it to POST tests too.

Verifying Response Headers

def test_post_user_header():
    """TC03: Content-Type should be JSON"""
    new_user = {"name": "Test User", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    response.raise_for_status()

    content_type = response.headers.get("Content-Type", "")
    assert "application/json" in content_type, \
        f"Unexpected Content-Type: {content_type}"

    print(f"\n✅ TC03 PASS | Content-Type: {content_type}")

💡 Key Takeaway:Content-Type returns as application/json; charset=utf-8, so always use in for partial matching instead of == for exact matching.

Verifying the Response Body

def test_post_user_response_body():
    """TC04: Sent data should be correctly included in the response"""
    new_user = {
        "name": "Yoshitsugu Tester",
        "username": "yoshitsugu728",
        "email": "yoshitsugu@example.com"
    }
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    response.raise_for_status()
    body = response.json()

    assert body["name"] == new_user["name"],   f"name mismatch: {body['name']}"
    assert body["email"] == new_user["email"], f"email mismatch: {body['email']}"

    print(f"\n✅ TC04 PASS | Created user: {body['name']}")

Verifying that an ID is Assigned

def test_post_user_id_assigned():
    """TC05: The created resource should be assigned an ID"""
    new_user = {"name": "Test User", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    response.raise_for_status()
    body = response.json()

    assert "id" in body,               "Response does not contain id"
    assert isinstance(body["id"], int), f"id should be int: {type(body['id'])}"
    assert body["id"] > 0,             f"id should be a positive integer: {body['id']}"

    print(f"\n✅ TC05 PASS | Assigned ID: {body['id']}")

💡 Key Takeaway:ID assignment verification is a test that’s often overlooked in practice. Always confirm that the server is auto-incrementing IDs and that the type is correct.


03. POST Validation Testing in API Tests

In production, always verify not just positive cases but also whether appropriate errors are returned when invalid data is sent.

Sending Without Required Fields

def test_post_user_missing_required_fields():
    """TC06: Appropriate error should return when required fields are missing"""
    incomplete_user = {
        "username": "yoshitsugu728"
        # name and email intentionally omitted
    }
    response = requests.post(f"{BASE_URL}/users", json=incomplete_user)

    assert response.status_code in [201, 400], \
        f"Expected 201 or 400, Got: {response.status_code}"

    print(f"\n✅ TC06 PASS | Status with missing fields: {response.status_code}")

Sending Empty Strings

def test_post_user_empty_string():
    """TC07: Appropriate error should return when empty strings are sent"""
    invalid_user = {"name": "", "email": ""}
    response = requests.post(f"{BASE_URL}/users", json=invalid_user)

    assert response.status_code in [201, 400, 422], \
        f"Unexpected status code: {response.status_code}"

    print(f"\n✅ TC07 PASS | Status with empty strings: {response.status_code}")

Sending an Invalid Email Format

def test_post_user_invalid_email():
    """TC08: Appropriate error should return for an invalid email format"""
    invalid_user = {"name": "Test User", "email": "not-an-email"}
    response = requests.post(f"{BASE_URL}/users", json=invalid_user)

    assert response.status_code in [201, 400, 422], \
        f"Unexpected status code: {response.status_code}"

    print(f"\n✅ TC08 PASS | Status with invalid email: {response.status_code}")

⚠️ Note:JSONPlaceholder is a mock API so it returns 201 even for invalid data. For accurate validation testing, you need an API with actual validation logic such as Restful Booker or reqres.in.


04. POST Schema Validation in API Tests

def test_post_user_schema():
    """TC09: Response schema (types) should be correct"""
    new_user = {"name": "Yoshitsugu Tester", "email": "yoshitsugu@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    response.raise_for_status()
    body = response.json()

    assert isinstance(body["id"],   int), f"id should be int: {type(body['id'])}"
    assert isinstance(body["name"], str), f"name should be str: {type(body['name'])}"

    print(f"\n✅ TC09 PASS | Schema validation complete")

05. Complete POST Test Code

"""
POST API Test
Target: JSONPlaceholder (https://jsonplaceholder.typicode.com)
Framework: Python + requests + pytest
"""
import requests

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


def test_post_user_status_code():
    """TC01: Status code 201 should be returned on data creation"""
    new_user = {"name": "Yoshitsugu Tester", "email": "yoshitsugu@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)
    assert response.status_code == 201, f"Expected: 201, Got: {response.status_code}"
    print(f"\n✅ TC01 PASS | status: {response.status_code}")


def test_post_user_raise_for_status():
    """TC02: Exception should be raised on 4xx/5xx errors"""
    new_user = {"name": "Test User", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)
    response.raise_for_status()
    assert response.status_code == 201
    print(f"\n✅ TC02 PASS | raise_for_status() passed normally")


def test_post_user_header():
    """TC03: Content-Type should be JSON"""
    new_user = {"name": "Test User", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)
    response.raise_for_status()
    content_type = response.headers.get("Content-Type", "")
    assert "application/json" in content_type
    print(f"\n✅ TC03 PASS | Content-Type: {content_type}")


def test_post_user_response_body():
    """TC04: Sent data should be correctly included in the response"""
    new_user = {"name": "Yoshitsugu Tester", "email": "yoshitsugu@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)
    response.raise_for_status()
    body = response.json()
    assert body["name"] == new_user["name"]
    assert body["email"] == new_user["email"]
    print(f"\n✅ TC04 PASS | name: {body['name']}")


def test_post_user_id_assigned():
    """TC05: The created resource should be assigned an ID"""
    new_user = {"name": "Test User", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)
    response.raise_for_status()
    body = response.json()
    assert "id" in body
    assert isinstance(body["id"], int)
    assert body["id"] > 0
    print(f"\n✅ TC05 PASS | Assigned ID: {body['id']}")


def test_post_user_missing_required_fields():
    """TC06: Appropriate error when required fields are missing"""
    incomplete_user = {"username": "yoshitsugu728"}
    response = requests.post(f"{BASE_URL}/users", json=incomplete_user)
    assert response.status_code in [201, 400]
    print(f"\n✅ TC06 PASS | status: {response.status_code}")


def test_post_user_empty_string():
    """TC07: Appropriate error when empty strings are sent"""
    invalid_user = {"name": "", "email": ""}
    response = requests.post(f"{BASE_URL}/users", json=invalid_user)
    assert response.status_code in [201, 400, 422]
    print(f"\n✅ TC07 PASS | status: {response.status_code}")


def test_post_user_invalid_email():
    """TC08: Appropriate error for invalid email format"""
    invalid_user = {"name": "Test User", "email": "not-an-email"}
    response = requests.post(f"{BASE_URL}/users", json=invalid_user)
    assert response.status_code in [201, 400, 422]
    print(f"\n✅ TC08 PASS | status: {response.status_code}")


def test_post_user_schema():
    """TC09: Response schema (types) should be correct"""
    new_user = {"name": "Yoshitsugu Tester", "email": "yoshitsugu@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)
    response.raise_for_status()
    body = response.json()
    assert isinstance(body["id"],   int)
    assert isinstance(body["name"], str)
    print(f"\n✅ TC09 PASS | Schema validation complete")

Run Command

pytest test_post_api.py -v -s

Example Output

test_post_api.py::test_post_user_status_code             PASSED
test_post_api.py::test_post_user_raise_for_status        PASSED
test_post_api.py::test_post_user_header                  PASSED
test_post_api.py::test_post_user_response_body           PASSED
test_post_api.py::test_post_user_id_assigned             PASSED
test_post_api.py::test_post_user_missing_required_fields PASSED
test_post_api.py::test_post_user_empty_string            PASSED
test_post_api.py::test_post_user_invalid_email           PASSED
test_post_api.py::test_post_user_schema                  PASSED

9 passed in 4.12s ✅

06. Pitfalls & Lessons Learned

Here are the key issues I encountered during implementation. I hope this helps others who run into the same problems.


① Getting stuck on the difference between json= and data=

When sending POST requests with requests, I got stuck because I didn’t know the difference between json= and data=. Using data= sets the Content-Type to application/x-www-form-urlencoded, which causes JSON APIs to fail.

# ❌ data= does not send in JSON format
response = requests.post(url, data={"name": "Taro"})

# ✅ json= automatically sets Content-Type: application/json
response = requests.post(url, json={"name": "Taro"})

💡 Key Takeaway:Using json= automatically handles body conversion and Content-Type setting, so always use json= for JSON APIs.


② Unsure whether POST status code should be 200 or 201

# ❌ Expecting 200 as a fixed value
assert response.status_code == 200

# ✅ REST convention: 201 for creation success
assert response.status_code == 201

# If spec is ambiguous, both can be allowed
assert response.status_code in [200, 201]

💡 Key Takeaway:Always check the API documentation for the expected status code before writing tests.


③ Response body doesn’t contain the sent data

# ❌ Exact match with sent data fails
assert body == new_user  # Fails because server adds an id field

# ✅ Verify only necessary fields individually
assert body["name"] == new_user["name"]
assert body["email"] == new_user["email"]
assert "id" in body

💡 Key Takeaway:POST responses include server-assigned IDs and auto-generated fields. Verifying individual fields rather than exact matches is more robust.


④ Unsure whether validation error status is 400 or 422

# ❌ Expecting only 400
assert response.status_code == 400

# ✅ Allow both based on the API spec
assert response.status_code in [400, 422]
# 400 Bad Request    → General request error
# 422 Unprocessable  → Validation error (common in FastAPI)

💡 Key Takeaway:Express typically returns 400, FastAPI typically returns 422. Check the API documentation.


⑤ Validation cannot be tested with a mock API

# JSONPlaceholder returns 201 even for invalid data
response = requests.post(url, json={"email": "not-an-email"})
print(response.status_code)  # → 201 (400 would be expected in a real API)

⚠️ Note:Accurate validation testing requires an API with actual validation logic. Use Restful Booker, reqres.in, or your own API.


07. Frequently Asked Questions (FAQ)

Q. What is the minimum I need to verify in a POST test?
A. At minimum, verify “status code is 201,” “sent data is included in the response,” and “an ID has been assigned.” Adding validation, schema verification, and header checks will bring your test to a production-level standard.

Q. How do I send JSON with requests.post()?
A. Use requests.post(url, json=data). Using json= automatically sets the Content-Type to application/json. Using data= sends it as form data, so always use json= for JSON APIs.

Q. What is the difference between GET and POST tests?
A. GET is for “data retrieval” and POST is for “data creation.” POST tests differ from GET in that they send a request body, expect a 201 status code on success, and require validation error testing.

Q. Which API can I use to practice validation testing?
A. JSONPlaceholder is a mock API with no validation. For proper validation testing, use reqres.in or your own API. For portfolio work, Restful Booker’s authentication endpoint also works.

Q. What is the difference between POST and PUT?
A. POST “creates new resources” while PUT “fully updates existing data.” POST creates a new resource every time, while PUT produces the same result no matter how many times it’s called (idempotency). PUT and PATCH testing will be covered in the next article.


08. Summary

When implementing API POST tests in Python, verifying the status code, response body, ID assignment, and validation using pytest and requests is the production standard.

This article covered how to implement API POST tests using pytest and requests.

Test CaseContent
TC01Status code should be 201
TC02Detect 4xx/5xx with raise_for_status()
TC03Content-Type should be JSON
TC04Sent data should be correctly included in the response
TC05The created resource should be assigned an ID
TC06Appropriate error when required fields are missing
TC07Appropriate error when empty strings are sent
TC08Appropriate error for invalid email format
TC09Response schema (types) should be correct

The next article covers PUT and PATCH request API testing — updating and partially updating existing data.

Copied title and URL