Skip to Main Content

What is API testing: an operational checklist for REST, GraphQL, and gRPC

API Testing
Publication date: August 24, 2026

API testing is the systematic validation of application programming interfaces through functional, contract, performance, and security tests specifically tailored to each protocol. In modernization processes toward microservices architectures, applying a generic checklist generates dangerous false negatives: while data exposure vulnerabilities like BOLA and BOPLA often go unnoticed in GraphQL environments, asynchronous bottlenecks and connection saturation remain hidden in high-volume gRPC services.

Concept and scope of API testing in the modernization toward microservices

API testing goes beyond the simple verification of isolated endpoints. It is a multidimensional discipline that encompasses functional testing of business logic, contract testing between components, load testing, and security posture evaluation, all executed iteratively during each deployment. The fundamental difference between sporadically validating an endpoint and establishing true governance lies in the ability to maintain the traceability of each finding (whether a defect, an active vulnerability, or an SLA degradation) throughout the entire application lifecycle.

Using checklists inherited from monolithic REST environments creates a structural risk in distributed ecosystems. While REST typically authorizes by route or endpoint, GraphQL requires granular authorization at the field level (field-level authorization); meanwhile, gRPC demands rigorous concurrency control over HTTP/2 channels and backpressure management in streaming, going far beyond mere payload serialization.

According to the Postman 2025 State of the API Report, although 93% of organizations use REST as their primary architectural style, 33% already operate with GraphQL in production. However, despite this multi-protocol environment, only 17% of companies apply contract testing in their CI/CD pipelines. This methodological gap largely explains why errors reappear in production when new microservices are added or development teams change.

To resolve these frictions in large-scale projects, Chakray’s enterprise integration architecture initiatives propose decoupling testing from the code to transform it into a continuous quality process.

The false negative in microservices migration: a real business risk

To understand the financial and operational impact of these failures, one only needs to look at common friction scenarios in the market. A quality assurance team migrating a monolithic core to microservices using generic REST tests might get a false negative (a “false pass” in green) that incorrectly approves a release to production.

In the realm of GraphQL, the absence of limits on query depth opens the door to mass data exposure vulnerabilities (Broken Object Property Level Authorization or BOPLA), classified in the OWASP API Security Top 10 (2023) under the API3:2023 category. An authenticated but unprivileged user could execute nested queries more than 15 levels deep and extract sensitive fields belonging to other users.

In the gRPC environment, failing to test concurrency control in bidirectional streaming under real usage patterns causes HTTP/2 channel resource exhaustion. This triggers cascading latencies and massive crashes under load—a behavior explicitly warned about in the Official gRPC Performance Documentation.

The cost of these errors is not just technical: it involves emergency rollbacks, unplanned audits, and loss of reputation. For this reason, Chakray’s legacy systems modernization frameworks advocate replacing isolated tests with continuous governance processes that protect the business regardless of the partner or team executing the tests.

Unified API testing checklist: REST vs. GraphQL vs. gRPC

Most tactical approaches in the market fragment quality by separating tests by protocol or tool. The governed approach integrates all controls under a single compliance criterion for the entire organization.

Testing dimension Tactical approach (fragmented) Governed approach (unified checklist)
Multi-protocol coverage Isolated guides and checklists by architectural style. Single, comprehensive checklist that audits REST, GraphQL, and gRPC in one piece.
Authorization (BOLA/BOPLA) Point-in-time endpoint verification at the time of testing. Object and field controls integrated as recurring criteria in each release.
Continuity during turnover Test knowledge stays within the tool or with the departing provider. Documented and transferable methodology, independent of who executes the tests.
Streaming bottlenecks Protobuf syntax is validated, but not behavior under >1000 streams. Load tests simulate real traffic (>500 RPC/s), measure HTTP/2 resource exhaustion, and stop deployments if p99 latency exceeds SLAs.
Integration with modernization Testing is executed as an isolated phase at the end of the project. Testing is anchored to each milestone of migrating legacy systems to microservices.

Functional, contract, and security checklist with code examples

1. REST APIs

  • Contract and functionality: Strictly validate compliance with the OpenAPI specification, the correct use of HTTP methods (GET, POST, PUT, DELETE), and the consistency of status codes (200, 201, 400, 401, 403, 404, 405, 409, 415, 422, 429).
  • Security (BOLA – API1:2023): Verify that altering identifiers in the URL (e.g., changing /api/v1/orders/123 to /api/v1/orders/124) does not return unauthorized information or reveal the resource’s existence through asymmetric responses.
  • API5:2023 BFLA: Test administrative methods (DELETE /api/v1/users/{id}) using a standard user token.
  • Mass assignment: Send {“role”: “admin”, “is_verified”: true} in a PATCH request and verify that the server ignores them (API3, write-side).
  • Pagination: ?limit=100000 → verify the server’s hard cap.
  • Contract-driven fuzzing: Running schemathesis run –checks all openapi.yaml generates negative cases directly from the OpenAPI specification.
import pytest
GHOST_ID = "00000000-0000-0000-0000-000000000000"   # Valid ID format, non-existent
LEAK_KEYS = {"owner", "user_id", "customer_id", "email", "tenant_id"}


def _leaks(node) -> bool:
    """Recursive search for owner metadata at any level."""
    if isinstance(node, dict):
        return bool(LEAK_KEYS & node.keys()) or any(_leaks(v) for v in node.values())
    if isinstance(node, list):
        return any(_leaks(v) for v in node)
    return False


def _safe_json(resp):
    try:
        return resp.json()
    except ValueError:
        return {}


def test_owner_can_access_own_resource(client, user_b_token, user_b_order_id):
    """Positive control: without this, a broken endpoint would pass the BOLA test."""
    r = client.get(f"/api/v1/orders/{user_b_order_id}",
                   headers={"Authorization": f"Bearer {user_b_token}"})
    assert r.status_code == 200


@pytest.mark.parametrize("method", ["get", "put", "patch", "delete"])
def test_bola_and_enumeration_prevention(client, user_a_token, user_b_order_id, method):
    """
    API1:2023 (BOLA) + enumeration prevention.
    The criterion is not just to 'deny', but to 'deny INDISTINGUISHABLY'.
    """
    headers = {"Authorization": f"Bearer {user_a_token}", "Accept": "application/json"}
    foreign = getattr(client, method)(f"/api/v1/orders/{user_b_order_id}", headers=headers)
    ghost   = getattr(client, method)(f"/api/v1/orders/{GHOST_ID}", headers=headers)

    # 1. Effective denial
    assert foreign.status_code in (403, 404), "Access to foreign resource allowed: BOLA active"

    # 2. Homogeneity: foreign and non-existent resources must be indistinguishable
    assert foreign.status_code == ghost.status_code, (
        f"Enumeration oracle: foreign={foreign.status_code} vs non-existent={ghost.status_code}"
    )
    assert _safe_json(foreign) == _safe_json(ghost)

    # 3. No owner metadata leakage at any level
    assert not _leaks(_safe_json(foreign))

 

2. GraphQL APIs

  • Contract and schema: validate the consistency of the full SDL schema, use rover subgraph check / graphql-inspector diff for breaking changes, enforce the @deprecated policy, and validate composition in federation.
  • Security and abuse: it is crucial to separate two attack vectors. On one hand, denial of service (API4:2023) is mitigated by implementing depth limits (Query Depth < 10) and complexity limits. On the other hand, to prevent mass data exposure (BOPLA – API3:2023), field-level authorization must be secured in the resolvers. If a user lacks permission, the server must return null for that field without revealing descriptive error messages.
  • Introspection disabled in production (API8:2023) and field suggestions disabled (“Did you mean ‘password’?” reconstructs the schema even if introspection is closed).
  • Amplification via aliases and batching: 1,000 aliases of the same login field in a single request can evade per-request rate limiting.
  • Persisted queries / allowlist serve as the ultimate control for first-party client APIs.
  • True cost vs. depth: Complexity analysis must weigh pagination arguments (e.g., first: 10000), otherwise, it is merely cosmetic.
  • N+1 and DataLoader: a resolver without batching turns a legitimate query into a storm of database queries. This is both a performance and an availability issue.
  • Authentication in subscriptions (WebSocket): the handshake often falls outside standard HTTP authentication middleware.
DEEP_NESTED_QUERY = """
query ExcessiveDepth {
  user { orders { items { product { category { suppliers { details { id } } } } } } }
}
"""

def test_query_depth_limit_enforced(graphql_client, user_token):
    """API4:2023 - Unrestricted Resource Consumption."""
    r = graphql_client.post("/graphql", json={"query": DEEP_NESTED_QUERY},
                            headers={"Authorization": f"Bearer {user_token}"})

    # 200 (legacy transport) or 400 (application/graphql-response+json) are both valid
    assert r.status_code in (200, 400)
    body = r.json()
    assert body.get("data") is None, "The query was executed: there is no depth limit"

    errors = body.get("errors") or []
    assert errors, "No errors: depth limit is not active"

    codes = {e.get("extensions", {}).get("code", "") for e in errors}
    msg = " ".join(e.get("message", "") for e in errors).lower()
    assert (codes & {"GRAPHQL_VALIDATION_FAILED", "QUERY_TOO_COMPLEX", "DEPTH_LIMIT_EXCEEDED"}
            or any(k in msg for k in ("depth", "complexity", "exceeded", "profundidad")))


SENSITIVE_FIELD_QUERY = """
query SensitiveFields($id: ID!) {
  employee(id: $id) { id name salary }
}
"""

def test_bopla_field_level_authorization(graphql_client, low_privilege_token, foreign_employee_id):
    """
    API3:2023 - BOPLA. Schema requirement: `salary` MUST be nullable,
    or the null will propagate and nullify the entire `employee` object.
    """
    r = graphql_client.post("/graphql",
                            json={"query": SENSITIVE_FIELD_QUERY,
                                  "variables": {"id": foreign_employee_id}},
                            headers={"Authorization": f"Bearer {low_privilege_token}"})
    assert r.status_code == 200
    employee = r.json()["data"]["employee"]

    assert employee["name"], "The partial response must preserve authorized fields"
    assert employee["salary"] is None, "BOPLA: sensitive field exposed without authorization"

    # The error, if it exists, must not confirm the existence or the name of the field
    for e in r.json().get("errors", []):
        assert "salary" not in e.get("message", "").lower()

 

GraphQL Validation Flow: Depth Limiting and Field-Level Auth (BOPLA) 

GraphQL Validation Flow: Depth Limiting and Field-Level Auth (BOPLA)

 

3. gRPC Services

  • Contract: strict synchronization of Protocol Buffers (.proto) definitions between clients and servers.
  • Performance and security: validate the strict requirement of mTLS (Mutual TLS) in service-to-service communication. Verify through automated tests that the server rejects (fails securely) any connection attempt without a valid, expired, or revoked client certificate. Apply timeout policies for each RPC call and validate the resilience of the HTTP/2 channel by measuring backpressure management in high-concurrency scenarios.
  • Reflection disabled in production (equivalent to GraphQL introspection: it exposes the entire .proto).
  • Message size limit (max_receive_message_length, 4 MB by default) and compression: DoS vector via decompression bomb.
  • Contract compatibility with buf: run buf lint + buf breaking –against ‘.git#branch=main’ in the PR.
  • Retry/hedging via service config and keepalive/GOAWAY: without these, a rolling update generates errors that will be confused with load failures.
  • mTLS / SPIFFE-SVID certificate rotation: test behavior during rotation.

Note: although the following Python script illustrates how to validate the functional logic of concurrency and timeouts, to inject infrastructure-level stress and saturate HTTP/2 channels in real CI/CD pipelines, we recommend integrating specialized tools like ghz.

import grpc
from concurrent.futures import ThreadPoolExecutor
import user_service_pb2
import user_service_pb2_grpc

def execute_rpc_call(stub, user_id):
    """
    Executes a gRPC call with a strict 2.0s timeout and catches channel exhaustion.
    """
    request = user_service_pb2.UserRequest(id=user_id)
    try:
        response = stub.GetUser(request, timeout=2.0)
        return ("SUCCESS", response)
    except grpc.RpcError as e:
        if e.code() == grpc.StatusCode.RESOURCE_EXHAUSTED:
            return ("BACKPRESSURE_TRIGGERED", e.details())
        elif e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
            return ("TIMEOUT", e.details())
        return ("ERROR", e.code())

def test_grpc_streaming_backpressure_under_load():
    """
    Simulates massive load on HTTP/2 channels to validate concurrency control.
    """
    channel = grpc.insecure_channel("localhost:50051")
    stub = user_service_pb2_grpc.UserServiceStub(channel)
    user_ids = [f"usr_{i}" for i in range(500)]
    
    with ThreadPoolExecutor(max_workers=200) as executor:
        futures = [executor.submit(execute_rpc_call, stub, uid) for uid in user_ids]
        results = [f.result() for f in futures]
    
    successful_calls = [r for r in results if r[0] == "SUCCESS"]
    backpressure_calls = [r for r in results if r[0] == "BACKPRESSURE_TRIGGERED"]
    
    # Ensure that failed calls were handled gracefully by backpressure
    assert len(successful_calls) + len(backpressure_calls) == len(user_ids)
    channel.close()

Continuous testing governance: a transferable methodology between partners

To prevent technical findings from disappearing when a team rotates or a testing provider changes, organizations need to implement an operational traceability matrix. Following reference frameworks in security and distributed architecture, such as the NIST SP 800-204B guide for Attribute-Based Access Control (ABAC) in Microservices Architectures, test results must be consolidated into a transparent control matrix:

Finding Applied Control Protocol Responsible Acceptance Criteria Release Status
BOPLA in the salary field Field-level authorization in resolver + Depth < 8 + Complexity < 800. GraphQL Lead QA / Arch Sec Returns null for unauthorized users; automated test in CI/CD; p99 latency < 150ms. Validated in v2.3
Timeout under load (>500 RPC/s) Backpressure manager in HTTP/2 + 2s timeout per call. gRPC Platform Engineer Concurrent load with zero memory failures; p99 latency < 200ms. Validated in v2.3
BOLA in /api/v1/invoices/{id} Ownership validation in the authentication middleware. REST Backend Lead Homogeneous 403/404 response; regression test integrated into the Pull Request. Pending v2.4

This methodological approach is complemented by Chakray’s API governance practices, which integrate validations under the Shift-Left Security Testing strategy in CI/CD pipelines. Furthermore, incorporating Fuzzing and DAST (Dynamic Application Security Testing) techniques allows for the automated injection of malformed payloads to discover undocumented vulnerabilities (Zero-days) prior to production deployment. This ensures a transparent and frictionless handoff process between technology partners.

FAQ

What is the difference between functional testing and contract testing in APIs?

Functional tests evaluate whether an isolated endpoint’s business logic responds correctly to the received inputs. Contract testing verifies that the data schema and types agreed upon between the provider service and the consumer are not broken after new deployments.

How can field-level authorization be validated in GraphQL without exposing sensitive information?

The resolver must be configured to verify the user’s credentials before resolving the sensitive field. If the request lacks permissions, the GraphQL response must return null for that field and avoid explicit error messages that confirm the existence of the data.

What metrics are critical in load testing gRPC with streaming?

It is essential to monitor latency per message (p95 and p99 percentiles), message volume per second, memory behavior under saturation, and, especially, the rate of RESOURCE_EXHAUSTED errors. This specific error highlights bottlenecks in HTTP/2 channel management.

Ensure quality and governance in your API architecture with Chakray

Modernizing an architecture toward distributed microservices using REST, GraphQL, and gRPC requires evolving testing strategies to avoid false negatives that compromise security or availability in production.

At Chakray, we guide organizations in designing resilient architectures through our integration and API consulting services, implementing technical governance frameworks, test automation, and quality assurance tailored to complex enterprise ecosystems. If you need to accelerate the maturity of your QA strategy or are looking for the backing of an integration implementation management team, contact our team of experts.

Talk to our experts!

Contact our team and discover the cutting-edge technologies that will empower your business.

contact us