Class MockRestServer

java.lang.Object
com.shri.restinpeace.mock.MockRestServer
All Implemented Interfaces:
AutoCloseable

public final class MockRestServer extends Object implements AutoCloseable
A real, local HTTP server for unit-testing code that calls a @RestClient interface, without depending on a real network - point RIP.getClient(Class, String) at baseUrl() and this server stands in for the real one.

Deliberately a genuine HTTP server (the same com.sun.net.httpserver this project's own integration test suite already uses), listening on loopback, rather than a fake transport swapped in underneath Unirest - so every request this server receives went through RIP's real request-building and Unirest's real serialization, exactly as it would against a real server. @Retry, @Timeout, and every registered RequestInterceptor all run completely unmodified.

 MockRestServer server = MockRestServer.start();
 server.on(HTTPMethod.GET, "/orders/{id}", MockResponse.ok("{\"status\":\"CONFIRMED\"}"));

 OrderApi api = RIP.getClient(OrderApi.class, server.baseUrl());
 Order order = api.getOrder("abc123");

 RecordedRequest sent = server.takeRequest();
 assertEquals("/orders/abc123", sent.getPath());

 server.close();
 
  • Method Details

    • start

      public static MockRestServer start()
      Starts a new server on an OS-assigned loopback port.
      Returns:
      the started server
    • baseUrl

      public String baseUrl()
      Returns this server's base URL, to pass to RIP.getClient(Class, String) or RipClientConfig.Builder.baseUrl(String).
      Returns:
      the base URL, e.g. "http://localhost:54321"
    • enqueue

      public MockRestServer enqueue(MockResponse response)
      Queues a response for the next request that doesn't match any route registered via on(com.shri.restinpeace.constant.HTTPMethod, java.lang.String, com.shri.restinpeace.mock.MockResponse) - a route always wins over this queue for a request it matches, so this only ever answers a path with no matching route at all. To script a sequence of responses to a path that also has a sticky final answer (e.g. a 503 then a 200, to prove @Retry recovers, while still returning 200 for every request after that), register the route with on(com.shri.restinpeace.constant.HTTPMethod, java.lang.String, com.shri.restinpeace.mock.MockResponse) first and use enqueueFor(com.shri.restinpeace.constant.HTTPMethod, java.lang.String, com.shri.restinpeace.mock.MockResponse) instead - this method can't do that for a path any route already covers.
      Parameters:
      response - the response to queue
      Returns:
      this server
    • on

      public MockRestServer on(HTTPMethod httpMethod, String pathTemplate, MockResponse response)
      Registers a response for every request matching httpMethod and pathTemplate - unlike enqueue(com.shri.restinpeace.mock.MockResponse), this doesn't get consumed; it keeps answering every matching request the same way. pathTemplate may contain {name} placeholders (matching RIP's own path-param syntax), each matching exactly one path segment, e.g. "/orders/{id}" matches /orders/abc123 but not /orders/abc/123. Checked before the enqueue(com.shri.restinpeace.mock.MockResponse) queue, in the order routes were registered - the first matching route wins. Calling this again for the same httpMethod and pathTemplate replaces that route's response in place (keeping its position relative to other routes) instead of adding a second, permanently-shadowed registration - the way to change one route's behavior mid-test.
      Parameters:
      httpMethod - the HTTP method to match
      pathTemplate - the path to match, with optional {name} placeholders
      response - the response to send for every matching request
      Returns:
      this server
    • on

      public MockRestServer on(HTTPMethod httpMethod, String pathTemplate, Map<String,String> requiredQueryParams, MockResponse response)
      Same as on(HTTPMethod, String, MockResponse), but only matches a request whose query params contain every entry in requiredQueryParams with an equal value - any other query params the request carries are ignored. Useful for an endpoint that behaves differently depending on a query param, e.g. registering one route for ?status=active and another for ?status=archived on the same path. As with the simpler overload, routes are checked in registration order and the first match wins - register the more specific (query-constrained) route first if a less specific one for the same path would otherwise shadow it. Calling this again with the same httpMethod, pathTemplate, and requiredQueryParams replaces that route in place, the same as the simpler overload.
      Parameters:
      httpMethod - the HTTP method to match
      pathTemplate - the path to match, with optional {name} placeholders
      requiredQueryParams - the query params that must be present with these exact values for this route to match
      response - the response to send for every matching request
      Returns:
      this server
    • on

      public MockRestServer on(HTTPMethod httpMethod, String pathTemplate, Predicate<RecordedRequest> matcher, MockResponse response)
      Same as on(HTTPMethod, String, MockResponse), but only matches a request for which matcher also returns true - for matching on a request header or the request body, neither of which the requiredQueryParams overload covers, e.g. request -> "v2".equals(request.getHeader("X-Api-Version")) or request -> request.getBody().contains("\"tier\":\"premium\""). As with the other overloads, routes are checked in registration order and the first match wins - register the more specific (matcher) route first if a less specific one for the same path would otherwise shadow it.

      Unlike the other on(...) overloads, calling this again never replaces an earlier registration - two arbitrary Predicates can't be compared for equality the way a requiredQueryParams map can, so there's no reliable way to tell whether it's "the same" route being re-registered. Use remove(com.shri.restinpeace.constant.HTTPMethod, java.lang.String) first if that's the intent.

      Parameters:
      httpMethod - the HTTP method to match
      pathTemplate - the path to match, with optional {name} placeholders
      matcher - an additional condition the request must satisfy
      response - the response to send for every matching request
      Returns:
      this server
    • enqueueFor

      public MockRestServer enqueueFor(HTTPMethod httpMethod, String pathTemplate, MockResponse response)
      Scripts a one-time response for a route already registered via on(HTTPMethod, String, MockResponse), consumed - in the order queued - before that route's sticky response, the way to combine a scripted failure sequence with a sticky final answer for the same path (e.g. a 503 then a 200, to prove @Retry recovers, while every request after that still gets the 200 the route was registered with). Unlike enqueue(com.shri.restinpeace.mock.MockResponse), which only ever answers a path with no route at all, this attaches to a specific route regardless of what other routes are registered.
      Parameters:
      httpMethod - the HTTP method of the route to script a response for
      pathTemplate - the path template of the route, exactly as passed to on(HTTPMethod, String, MockResponse)
      response - the one-time response to queue
      Returns:
      this server
      Throws:
      NoSuchElementException - if no route matching httpMethod and pathTemplate has been registered via on(com.shri.restinpeace.constant.HTTPMethod, java.lang.String, com.shri.restinpeace.mock.MockResponse) yet
    • enqueueFor

      public MockRestServer enqueueFor(HTTPMethod httpMethod, String pathTemplate, Map<String,String> requiredQueryParams, MockResponse response)
      Same as enqueueFor(HTTPMethod, String, MockResponse), for a route registered via on(HTTPMethod, String, Map, MockResponse) with the same requiredQueryParams.
      Parameters:
      httpMethod - the HTTP method of the route to script a response for
      pathTemplate - the path template of the route, exactly as passed to on(HTTPMethod, String, Map, MockResponse)
      requiredQueryParams - the required query params of the route, exactly as passed to on(HTTPMethod, String, Map, MockResponse)
      response - the one-time response to queue
      Returns:
      this server
      Throws:
      NoSuchElementException - if no matching route has been registered via on(com.shri.restinpeace.constant.HTTPMethod, java.lang.String, com.shri.restinpeace.mock.MockResponse) yet
    • onFlaky

      public MockRestServer onFlaky(HTTPMethod httpMethod, String pathTemplate, int failuresBeforeSuccess, MockResponse failureResponse, MockResponse successResponse)
      Registers a route that fails failuresBeforeSuccess times with failureResponse before settling into successResponse for every request after that - sugar for calling on(HTTPMethod, String, MockResponse) with successResponse and then enqueueFor(com.shri.restinpeace.constant.HTTPMethod, java.lang.String, com.shri.restinpeace.mock.MockResponse) with failureResponse failuresBeforeSuccess times.
      Parameters:
      httpMethod - the HTTP method to match
      pathTemplate - the path to match, with optional {name} placeholders
      failuresBeforeSuccess - how many requests get failureResponse before successResponse takes over
      failureResponse - the response sent for the first failuresBeforeSuccess matching requests
      successResponse - the response sent for every matching request after that
      Returns:
      this server
    • onPages

      public MockRestServer onPages(HTTPMethod httpMethod, String pathTemplate, MockResponse... responses)
      Scripts an ordered sequence of responses for a route - the 1st matching request gets responses[0], the 2nd gets responses[1], and so on, with the last response answering every request after the sequence is exhausted. Sugar for on(HTTPMethod, String, MockResponse) with the last response and enqueueFor(HTTPMethod, String, MockResponse) with the rest, in the right order - the natural way to script a @Paginated/PaginationStrategy<T> fetch's page-by-page response sequence in one call, without needing a separate route per page distinguished by cursor/offset query param value (see docs/design/pagination-helper.md ยง11).
       server.onPages(HTTPMethod.GET, "/orders",
               MockResponse.ok("{\"orders\":[{\"id\":\"1\"}],\"next\":true}"),
               MockResponse.ok("{\"orders\":[{\"id\":\"2\"}],\"next\":false}"));
       
      Parameters:
      httpMethod - the HTTP method to match
      pathTemplate - the path to match, with optional {name} placeholders
      responses - the page sequence, in order; must not be empty
      Returns:
      this server
      Throws:
      IllegalArgumentException - if responses is empty
    • onPages

      public MockRestServer onPages(HTTPMethod httpMethod, String pathTemplate, Map<String,String> requiredQueryParams, MockResponse... responses)
      Same as onPages(HTTPMethod, String, MockResponse...), but only matches a request whose query params contain every entry in requiredQueryParams - for a path also serving other, non-paginated traffic that a bare onPages(HTTPMethod, String, MockResponse...) route would otherwise intercept too.
      Parameters:
      httpMethod - the HTTP method to match
      pathTemplate - the path to match, with optional {name} placeholders
      requiredQueryParams - the query params that must be present with these exact values for this route to match
      responses - the page sequence, in order; must not be empty
      Returns:
      this server
      Throws:
      IllegalArgumentException - if responses is empty
    • remove

      public boolean remove(HTTPMethod httpMethod, String pathTemplate)
      Removes a route registered via on(HTTPMethod, String, MockResponse), so a later request to that path falls through to any other registered route, the enqueue(com.shri.restinpeace.mock.MockResponse) queue, or the default "no response was queued or registered" failure - without a full reset(), which would also wipe every other route, the queue, and recorded-request history. A no-op (returning false) if no such route is registered.
      Parameters:
      httpMethod - the HTTP method of the route to remove
      pathTemplate - the path template of the route, exactly as passed to on(HTTPMethod, String, MockResponse)
      Returns:
      true if a route was removed, false if none matched
    • remove

      public boolean remove(HTTPMethod httpMethod, String pathTemplate, Map<String,String> requiredQueryParams)
      Same as remove(HTTPMethod, String), for a route registered via on(HTTPMethod, String, Map, MockResponse) with the same requiredQueryParams.
      Parameters:
      httpMethod - the HTTP method of the route to remove
      pathTemplate - the path template of the route, exactly as passed to on(HTTPMethod, String, Map, MockResponse)
      requiredQueryParams - the required query params of the route, exactly as passed to on(HTTPMethod, String, Map, MockResponse)
      Returns:
      true if a route was removed, false if none matched
    • reset

      public MockRestServer reset()
      Clears every queued response, registered route, and recorded request, so the same server can be reused across multiple tests instead of paying to start a new one for each - e.g. from a @BeforeEach method, with the server itself started once in a @BeforeAll.
      Returns:
      this server
    • takeRequest

      public RecordedRequest takeRequest()
      Removes and returns the oldest recorded request, for asserting against one call at a time in the order calls were made.
      Returns:
      the oldest recorded request
      Throws:
      NoSuchElementException - if no request has been recorded
    • getRecordedRequests

      public List<RecordedRequest> getRecordedRequests()
      Returns every request recorded so far, oldest first, without consuming them - unlike takeRequest().
      Returns:
      the recorded requests
    • requestCount

      public int requestCount()
      Returns how many requests have been recorded so far.
      Returns:
      the recorded request count
    • countOf

      public int countOf(HTTPMethod httpMethod, String pathTemplate)
      Returns how many recorded requests match httpMethod and pathTemplate - a check for "was this endpoint called, and how many times" without manually filtering getRecordedRequests() or looping over takeRequest().
      Parameters:
      httpMethod - the HTTP method to match
      pathTemplate - the path to match, with optional {name} placeholders, using the same syntax as on(HTTPMethod, String, MockResponse)
      Returns:
      how many recorded requests matched
    • getUnhitRoutes

      public List<String> getUnhitRoutes()
      Returns every registered route that hasn't matched any recorded request yet, as "METHOD path" strings (e.g. "GET /orders/{id}") - a coverage check for a route left registered after the code path that used to exercise it was removed, which otherwise causes no failure at all.
      Returns:
      the unhit routes, in registration order, or an empty list if every route has been hit at least once
    • close

      public void close()
      Stops the server.
      Specified by:
      close in interface AutoCloseable