Python API GET Test Tutorial | pytest & requests Status Code and Response Verification

test-automation

📌 Who This Article Is For

  • Those who want to write GET request tests with Python and requests
  • Those who want to learn how to verify status codes and JSON responses
  • QA engineers who want to automate API testing with pytest
  • Those who want to implement schema validation for API responses

What You Will Learn

  • The basic pattern for writing GET request API tests with pytest and requests
  • How to verify status codes, response bodies, and headers
  • How to validate JSON schema (types and fields)
  • Test case design patterns for both positive and negative API test scenarios

👨‍💻 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, the first thing to implement is a GET request test. Using pytest and requests, we can automatically verify “Is the status code 200?” and “Does the JSON response contain all the required fields?”

This article covers everything from basic status code checks to response body schema validation with production-ready code.


00. API Testing and the Basics of GET Requests

API testing is a way to send HTTP requests directly without a browser and verify the responses. GET requests are the most fundamental operation in any API, responsible for “retrieving data.”

In GET testing, we mainly verify three things.

Verification ItemContentExample
Status CodeDoes the expected HTTP status return?200, 404, 401, etc.
Response BodyAre the JSON values correct?id, name, email values
Schema ValidationAre the JSON fields and types correct?id is int, name is str

💡 Key Takeaway:In real-world projects, it’s important to verify not just the status code, but the content of the response as well. Even if the status is 200, incorrect response values are still bugs.


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.

GET https://jsonplaceholder.typicode.com/users/1

Here is a sample response.

{
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org"
}

💡 Key Takeaway:JSONPlaceholder is a mock API where data changes and deletions don’t actually take effect. It’s safe to use for learning purposes.


02. How to Write API GET Tests in Python

Verifying the Status Code

The most fundamental test. Send a request and confirm that 200 is returned.

import requests

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

def test_get_user_status_code():
    """TC01: Status code should be 200"""
    response = requests.get(f"{BASE_URL}/users/1")

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

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

Detecting 4xx/5xx Errors with raise_for_status()

This is a commonly used pattern in real-world projects. By adding raise_for_status(), any 4xx or 5xx response will automatically raise an exception and fail the test.

def test_get_user_raise_for_status():
    """TC02: Exception should be raised on 4xx/5xx errors"""
    response = requests.get(f"{BASE_URL}/users/1")

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

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

💡 Key Takeaway:response.raise_for_status() is an essential one-liner in real-world API testing. Adding it before your assert ensures unexpected error responses are always caught.

Verifying Response Headers

Confirm that the Content-Type of the response is correct. This helps detect cases where HTML is returned instead of the expected JSON.

def test_get_user_header():
    """TC03: Content-Type should be JSON"""
    response = requests.get(f"{BASE_URL}/users/1")

    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:Note that the actual Content-Type header returns application/json; charset=utf-8, so always use in for partial matching instead of == for exact matching.

Verifying the JSON Response Body

In addition to the status code, verify that the response values are correct.

def test_get_user_response_body():
    """TC04: Response body values should be correct"""
    response = requests.get(f"{BASE_URL}/users/1")

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

    assert body["id"] == 1,                      f"id mismatch: {body['id']}"
    assert body["name"] == "Leanne Graham",      f"name mismatch: {body['name']}"
    assert body["email"] == "Sincere@april.biz", f"email mismatch: {body['email']}"

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

Verifying Required Fields Exist

Check that all required fields are present in the response.

def test_get_user_fields_exist():
    """TC05: All required fields should exist"""
    response = requests.get(f"{BASE_URL}/users/1")
    response.raise_for_status()
    body = response.json()

    required_fields = ["id", "name", "username", "email", "phone", "website"]
    for field in required_fields:
        assert field in body, f"Required field '{field}' is missing"

    print(f"\n✅ TC05 PASS | All required fields verified")

💡 Key Takeaway:Managing fields as a list means adding new required fields requires minimal code changes. This is a commonly used pattern in production code.


03. API Test Schema (Type) Validation

In addition to verifying values, also test whether the types (int/str/dict) are correct. Type changes can break the frontend, making this an important check in real-world projects.

def test_get_user_schema():
    """TC06: Response schema (types) should be correct"""
    response = requests.get(f"{BASE_URL}/users/1")
    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'])}"
    assert isinstance(body["username"], str),  f"username should be str: {type(body['username'])}"
    assert isinstance(body["email"],    str),  f"email should be str: {type(body['email'])}"
    assert isinstance(body["address"],  dict), f"address should be dict: {type(body['address'])}"
    assert isinstance(body["company"],  dict), f"company should be dict: {type(body['company'])}"

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

💡 Key Takeaway:Using isinstance() makes type checking simple. It allows you to immediately detect when an API response type changes, making it an essential check in production.


04. Negative Test Patterns for API Testing

Accessing a Non-Existent Resource (404 Verification)

def test_get_nonexistent_user():
    """TC07: Accessing a non-existent user should return 404"""
    response = requests.get(f"{BASE_URL}/users/99999")

    assert response.status_code == 404, \
        f"Expected 404 for non-existent resource, got: {response.status_code}"

    print(f"\n✅ TC07 PASS | 404 correctly verified: {response.status_code}")

Retrieving a List and Verifying the Count

def test_get_all_users():
    """TC08: Full user list should return 10 records"""
    response = requests.get(f"{BASE_URL}/users")
    response.raise_for_status()
    body = response.json()

    assert isinstance(body, list), "Response should be a list"
    assert len(body) == 10, f"Expected 10 users, got: {len(body)}"

    print(f"\n✅ TC08 PASS | User list retrieved: {len(body)} records")

⚠️ Note:Negative tests are just as important as positive tests. Verifying that non-existent resources correctly return 404 confirms that the API’s error handling is implemented correctly.


05. API Test Response Time Verification

From a performance perspective, also verify that the response comes back within a certain time limit.

import time

def test_get_user_response_time():
    """TC09: Response time should be within 2000ms"""
    start = time.time()
    response = requests.get(f"{BASE_URL}/users/1")
    elapsed_ms = (time.time() - start) * 1000

    assert response.status_code == 200
    assert elapsed_ms < 2000, \
        f"Response too slow: {elapsed_ms:.0f}ms (limit: 2000ms)"

    print(f"\n✅ TC09 PASS | Response time: {elapsed_ms:.0f}ms")

💡 Key Takeaway:Adjust the response time threshold based on your system requirements. Integrating this into CI/CD allows automatic detection of performance regressions.


06. Complete GET Test Code

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

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


def test_get_user_status_code():
    """TC01: Status code should be 200"""
    response = requests.get(f"{BASE_URL}/users/1")
    assert response.status_code == 200, \
        f"Expected: 200, Got: {response.status_code}"
    print(f"\n✅ TC01 PASS | status: {response.status_code}")


def test_get_user_raise_for_status():
    """TC02: Exception should be raised on 4xx/5xx errors"""
    response = requests.get(f"{BASE_URL}/users/1")
    response.raise_for_status()
    assert response.status_code == 200
    print(f"\n✅ TC02 PASS | raise_for_status() passed normally")


def test_get_user_header():
    """TC03: Content-Type should be JSON"""
    response = requests.get(f"{BASE_URL}/users/1")
    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}")


def test_get_user_response_body():
    """TC04: Response body values should be correct"""
    response = requests.get(f"{BASE_URL}/users/1")
    response.raise_for_status()
    body = response.json()
    assert body["id"] == 1
    assert body["name"] == "Leanne Graham"
    assert body["email"] == "Sincere@april.biz"
    print(f"\n✅ TC04 PASS | name: {body['name']}")


def test_get_user_fields_exist():
    """TC05: All required fields should exist"""
    response = requests.get(f"{BASE_URL}/users/1")
    response.raise_for_status()
    body = response.json()
    for field in ["id", "name", "username", "email", "phone", "website"]:
        assert field in body, f"Required field '{field}' is missing"
    print(f"\n✅ TC05 PASS | All required fields verified")


def test_get_user_schema():
    """TC06: Response schema (types) should be correct"""
    response = requests.get(f"{BASE_URL}/users/1")
    response.raise_for_status()
    body = response.json()
    assert isinstance(body["id"],      int)
    assert isinstance(body["name"],    str)
    assert isinstance(body["email"],   str)
    assert isinstance(body["address"], dict)
    assert isinstance(body["company"], dict)
    print(f"\n✅ TC06 PASS | Schema validation complete")


def test_get_nonexistent_user():
    """TC07: Non-existent user should return 404"""
    response = requests.get(f"{BASE_URL}/users/99999")
    assert response.status_code == 404
    print(f"\n✅ TC07 PASS | 404 verified: {response.status_code}")


def test_get_all_users():
    """TC08: Full user list should return 10 records"""
    response = requests.get(f"{BASE_URL}/users")
    response.raise_for_status()
    body = response.json()
    assert isinstance(body, list)
    assert len(body) == 10
    print(f"\n✅ TC08 PASS | User count: {len(body)}")


def test_get_user_response_time():
    """TC09: Response time should be within 2000ms"""
    start = time.time()
    response = requests.get(f"{BASE_URL}/users/1")
    elapsed_ms = (time.time() - start) * 1000
    assert response.status_code == 200
    assert elapsed_ms < 2000, f"{elapsed_ms:.0f}ms (limit: 2000ms)"
    print(f"\n✅ TC09 PASS | Response time: {elapsed_ms:.0f}ms")

Run Command

pytest test_get_api.py -v -s

Example Output

test_get_api.py::test_get_user_status_code      PASSED
test_get_api.py::test_get_user_raise_for_status PASSED
test_get_api.py::test_get_user_header           PASSED
test_get_api.py::test_get_user_response_body    PASSED
test_get_api.py::test_get_user_fields_exist     PASSED
test_get_api.py::test_get_user_schema           PASSED
test_get_api.py::test_get_nonexistent_user      PASSED
test_get_api.py::test_get_all_users             PASSED
test_get_api.py::test_get_user_response_time    PASSED

9 passed in 3.45s ✅

07. Pitfalls & Lessons Learned

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


① Getting a KeyError from response.json()

When extracting values from the response JSON, typos or differences in capitalization caused KeyError to occur.

# ❌ Typo in key name
assert body["Name"] == "Leanne Graham"  # KeyError! (N is uppercase)

# ✅ First check key names with print()
print(body.keys())
# dict_keys(['id', 'name', 'username', 'email', ...])

# ✅ Use get() to avoid KeyError
name = body.get("name", "")  # Returns empty string if key is missing

💡 Key Takeaway:For any unfamiliar API, always use print(response.json()) to check the response structure before writing test code.


② Test fails even though status code is 200

There were cases where the assert failed even though the status code was 200. The cause was that the response body was returning empty JSON or an unexpected structure.

# ❌ Only checking status code and assuming success
assert response.status_code == 200  # May pass even if body is empty

# ✅ Also verify the response body
assert response.status_code == 200
assert response.json() is not None
assert len(response.json()) > 0

💡 Key Takeaway:"Got a 200 response = success" is not correct. Verifying the content of the response is the essence of API testing.


③ isinstance() fails when checking nested JSON types

When trying to validate nested JSON fields like address.city, I couldn't access them correctly and got an error.

# ❌ Trying to access nested field directly — fails
assert isinstance(body["address.city"], str)  # KeyError!

# ✅ Correct way to access nested fields
assert isinstance(body["address"]["city"], str)
assert isinstance(body["address"]["zipcode"], str)

💡 Key Takeaway:Access nested JSON step by step like body["address"]["city"]. Always check the structure with print(body["address"]) first.


④ Response time varies by environment

Tests passed locally under 1000ms, but failed in CI because they exceeded 2000ms.

# ❌ Threshold too strict
assert elapsed_ms < 500  # May fail in CI

# ✅ Set a more generous threshold to account for environment differences
assert elapsed_ms < 3000  # 3 seconds is sufficient

⚠️ Note:Response time varies with network conditions and server load. Always set a threshold with some margin.


⑤ Test breaks when list count changes

When the test environment data changed, the count also changed and the test broke. In real projects, condition-based verification is more robust than fixed values.

# ❌ Hard-coding the count breaks easily
assert len(body) == 10  # Fails if data is added or removed

# ✅ Verifying "at least 1 exists" is more robust
assert len(body) >= 1, "No users exist"
assert isinstance(body, list), "Response should be a list"

💡 Key Takeaway:Fixed values are fine for mock APIs like JSONPlaceholder. In real projects, design tests that won't break when data changes.


08. Frequently Asked Questions (FAQ)

Q. What is the minimum I need to verify in an API GET test?
A. At minimum, verify that "the status code is 200" and "all required fields exist." Adding type checks, value verification, and response time checks will bring your test to a production-level standard.

Q. What is the difference between requests.get() and requests.head()?
A. get() retrieves the full response body. head() only retrieves headers without the body, making it faster. If you only need to check the status code, head() is more efficient, but some servers reject HEAD requests.

Q. How do I use assert vs pytest.raises?
A. Use assert for verifying normal response values. Use pytest.raises when you want to confirm that an exception is raised. For API testing, assert is sufficient in most cases.

Q. How many test cases should I write for GET tests?
A. Start with at minimum "1 positive case and 1 negative case." In production, 5–7 cases covering status verification, body validation, schema check, 404 check, and response time is the standard. TC01–TC09 in this article represent that standard pattern.

Q. Are there other practice API sites besides JSONPlaceholder?
A. Yes. Use Restful Booker for authentication flow testing, and reqres.in or httpbin.org for more advanced scenarios. For portfolio work, the combination of JSONPlaceholder (CRUD) and Restful Booker (auth) is recommended.


09. Summary

When implementing API GET tests in Python, verifying the status code, response body, and schema using pytest and requests is the production standard. You can write comprehensive tests with simple, readable code.

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

Test CaseContent
TC01Status code should be 200
TC02Detect 4xx/5xx with raise_for_status()
TC03Content-Type should be JSON
TC04Response body values should be correct
TC05All required fields should exist
TC06Schema (types) should be correct
TC07Non-existent resource should return 404
TC08List retrieval should return correct count
TC09Response time should be within 2000ms

The next article covers API POST request testing — creating data and validating input.

Copied title and URL