How to Write Tests for Spring Cloud Gateway Routes and Filters

Spring Cloud Gateway sits at an important boundary: it receives an HTTP request, decides where it should go, modifies it when necessary, and returns a response to the client. A small routing mistake can therefore affect authentication, observability, caching, rate limits, and the availability of several backend services at once.

Effective tests should verify more than whether an application starts. They need to prove that route predicates match the intended requests, filters preserve or transform the correct data, failures produce useful responses, and downstream services receive the request that the gateway intended to send.

For Australian teams, this is especially relevant when services are spread across Sydney, Melbourne, Brisbane, or regional locations. Network latency over NBN connections, cloud-region choices, privacy obligations under the Privacy Act 1988, and integrations in the local market all make gateway behaviour worth testing as a separate concern.

Build A Small Testable Gateway

Keep route configuration explicit and easy to load in a test context. A Java configuration class or a YAML file can define path, host, method, header, and query predicates, while filters can add headers, rewrite paths, strip prefixes, or enforce authentication.

A minimal route might look like this:

spring:
  cloud:
    gateway:
      routes:
        - id: orders
          uri: http://localhost:9001
          predicates:
            - Path=/api/orders/**
            - Method=GET
          filters:
            - StripPrefix=1
            - AddRequestHeader=X-Gateway, test

The route accepts GET /api/orders/42, removes the first path segment, and forwards /orders/42. Tests should make this contract visible. Avoid relying on a real service running on port 9001 because that makes failures ambiguous and slows down the build.

For unit-level checks, instantiate route locator beans and inspect their predicates or use a lightweight application context. For behaviour-level checks, run the gateway on a random port and replace the downstream service with WireMock, MockWebServer, or a dedicated mock HTTP server.

Verify Predicates At The HTTP Boundary

A route test should exercise the request as a client would send it. WebTestClient works well with the reactive gateway and can check status codes, response headers, and response bodies without requiring a browser or a full external environment.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrdersRouteTest {

    @Autowired
    WebTestClient client;

    @Test
    void routesGetOrdersToTheOrdersService() {
        client.get()
              .uri("/api/orders/42")
              .exchange()
              .expectStatus().isOk()
              .expectHeader().valueEquals("X-Service", "orders");
    }
}

The downstream stub should record the request, allowing the test to assert the forwarded path and method. Add negative cases as well: POST /api/orders/42 should not match a GET route, and /api/customers/42 should not be sent to the orders service. These examples test route selection rather than merely application availability.

Host and header predicates deserve their own cases. A route restricted to api.example.com should reject an unrelated host, while a version header such as X-Api-Version: 2 should select the expected backend. Test exact and near-match values because accidental prefix matching can expose the wrong API version.

Test Filters As Observable Contracts

Filters are easiest to test through effects that a client or downstream service can observe. For a request filter, inspect the received method, URI, headers, query parameters, and body. For a response filter, verify status, headers, and transformed content returned by the gateway.

A custom GlobalFilter might attach a correlation ID when the client does not provide one and preserve an existing ID when it does. A good test covers both paths:

client.get()
      .uri("/api/orders/42")
      .header("X-Correlation-Id", "abc-123")
      .exchange()
      .expectHeader()
      .valueEquals("X-Correlation-Id", "abc-123");

Do not assert only that a filter class is present in the application context. That proves wiring, not behaviour. If a filter removes sensitive headers, verify that Authorization or internal tracing values are absent from the forwarded request. This is particularly important when a gateway handles personal information covered by Australian privacy requirements.

Retry, timeout, circuit breaker, and rate-limit filters require failure-oriented tests. Make the stub return a 500 response, delay its response, or close the connection, then assert the gateway’s status and response body. A retry test should confirm the exact number of downstream attempts; otherwise, a future configuration change could increase traffic unexpectedly.

Choose The Right Test Layer

Different test styles answer different questions. A route definition test is fast and precise, while a running-gateway test catches integration problems involving codecs, filter ordering, security, and URI construction.

Test level Main target Typical tools Strength Limitation
Route unit test Predicates and route metadata JUnit, route locator Very fast feedback Does not prove real HTTP forwarding
Filter unit test Custom filter logic Mockito, Reactor Test Isolates edge cases Can miss framework configuration issues
Gateway slice test Web layer and selected filters @WebFluxTest, WebTestClient Focused Spring behaviour Requires careful bean setup
Integration test Full routing and downstream calls @SpringBootTest, WireMock Closest to production flow Slower and more setup-heavy
Contract test Gateway-backend agreement Pact or HTTP contracts Protects API compatibility Adds contract maintenance

Use unit tests for custom logic such as a token parser or header transformer. Use integration tests for path rewriting, filter ordering, security chains, and interactions with the reactive HTTP client. A route that passes a unit test can still fail at runtime because a filter changes the URI before another filter reads it.

Spring profiles are useful here. A test profile can point routes at local stub servers while retaining production-like predicates and filters. Testcontainers can provide a more realistic environment for Redis-backed rate limiting or service discovery, but avoid adding containers when a deterministic mock is enough.

Make Downstream Calls Deterministic

A reliable gateway test controls every downstream outcome. WireMock can match paths, methods, headers, and request bodies, then return configured responses. This makes it possible to prove that /api/orders/42 became /orders/42, rather than simply receiving a successful response from a permissive mock.

Request bodies need special care in a reactive application. If a filter reads and modifies a body, the body must remain available to the downstream exchange. Tests should send JSON, verify the transformed JSON at the stub, and include an empty-body case. Add malformed JSON and oversized payload cases when validation or limits are part of the gateway’s responsibility.

Test data should represent the actual shape of your APIs. If a gateway fronts a small food or wellness service, a fixture can include a realistic payload such as a natural probiotic drink guide, while the gateway test focuses on routing, content types, and response handling rather than the domain logic.

Timeout tests should use controlled delays instead of sleeping for arbitrary periods. Configure a short test timeout, delay the mock response beyond it, and assert a 504 or the response defined by the application. This keeps tests quick and avoids flaky results on shared CI runners.

Cover Failures, Security, And Compliance

The useful failure cases are often more valuable than another happy-path route test. Verify what happens when the backend returns 401, 403, 404, 429, 500, and 503. Check whether the gateway preserves the status, maps it to a standard error response, or exposes an internal exception accidentally.

Authentication tests should cover missing, expired, malformed, and valid credentials. If the gateway performs JWT validation, use test keys and fixed claims rather than contacting a live identity provider. Assert that public routes remain public and protected routes cannot be reached by changing only the URL or HTTP method.

Australian applications may process names, addresses, payment details, or health information. Tests should verify that logs and error responses do not disclose those values. For systems subject to the Australian Privacy Act 1988 or sector-specific expectations such as APRA controls, route and filter tests can act as an early check that sensitive headers and payload fragments are not copied into diagnostic output.

Use A Practical Gateway Checklist

Keep the first group focused on route-selection behaviour:

Then check transformation and operational behaviour:

CI should run fast unit and filter tests on every commit, followed by gateway integration tests on pull requests. Parallel jobs are useful, but ensure each stub server receives its own port and test data. This matters for teams working across Australian time zones, where a slow or flaky pipeline can delay handovers between Melbourne developers and Sydney-based operations staff.

Track test coverage by behaviour, not just by line count. A route file with 95 per cent coverage may still lack a test for an overlapping predicate, an unavailable backend, or a filter that runs in the wrong order.

Keep Route Tests Maintainable

Name tests after the observable contract: rejectsPostRequestsForReadOnlyOrdersRoute is more useful than testsOrdersRoute. Use small fixture builders for requests and responses, and centralise common gateway setup without hiding the important route-specific details.

Filter ordering should be tested whenever order changes the result. For example, authentication must usually run before a filter that trusts user-derived headers, while path rewriting must occur before a downstream matcher checks the URI. A single integration test with two deliberately interacting filters can protect this sequence.

Review tests when routes change ownership or move between cloud regions. A Sydney-based backend and a Melbourne-based backend may both pass local functional tests while exposing different latency and timeout behaviour. Include realistic timeout budgets and, where appropriate, a contract test that runs against the backend team’s published API definition.

The strongest suite gives fast feedback for route predicates, focused checks for custom filters, and a smaller number of full HTTP integration tests. It treats status codes, forwarded requests, security boundaries, and failure handling as part of the gateway’s public behaviour. The key thing to remember is that a Spring Cloud Gateway test should prove the complete request journey, from the incoming predicate match to the exact downstream call and the response returned to the client.