javaokhttpspring-webclientmockwebserver

Can you enqueue responses within the dispatcher for MockWebServer?


I'm writing tests for my webserver, and I'm making use of MockWebServer (okhttp3.mockwebserver) to mock the responses. As I understand it from the documentation, there are broadly 2 ways to do this:

  1. Enqueue all the responses in order (mockWebServer.enqueue(...))
  2. Define a dispatcher that specifies responses based on some other policy (such as dispatching on the request path

I find myself in a position where I need to use both of these approaches. The dispatcher is much more efficient than enqueueing each request for each test, but it can't be used for testing a retry mechanism (like Retry.backoff(3, Duration.ofSeconds(10)))). Enqueueing requests is easy, but tedious for every other test that does not involve retry.

Right now, the only solution I can think of is to use 2 separate MockWebServers for the 2 types of tests (one using enqueue for retry based tests, the other using a dispatcher for remaining API tests). This seems suboptimal, especially because the dispatcher init() is performed before every test (@BeforeAll), so the retry tests would have 2 MockWebServers simultaneously.

Is there any way to combine both these approaches into a single one? I'm basically looking for something along the lines of:

@Override
      public MockResponse dispatch(RecordedRequest req) throws InterruptedException {
        return switch (req.getPath()) {
          case "/query1" -> new MockResponse().setResponseCode(200).setBody(...);
          case "/query2" -> new MockResponse().setResponseCode(200).setBody(...);
          case "/query3" -> server.enqueue(new MockResponse().setResponseCode(502));
                            server.enqueue(new MockResponse().setResponseCode(502));
                            server.enqueue(new MockResponse().setResponseCode(200).setBody(...));

In this example, I want requests to /query1 and /query2 to always return the specified responses, but for /query3 I want those 3 specific responses in that order. But this obviously doesn't work because the Dispatcher's dispatch(...) method needs a single MockResponse return value.

I'm also restricted to using the libraries already in the project, so the answer to this similar question doesn't work for me (also that library hasn't been updated in over 5 years).


Solution

  • You could implement a Dispatcher where you add actions to a list, each with a predicate to match the request. Once an action has matched you discard it so the next action can be applied.

    public class MyDispatcher implements Dispatcher {
       public static class MyDispatcherEntry {
          private Predicate<RecordedRequest> predicate;
          private Function<RecordedRequest, MockResponse> action;
    
          // TODO constructor
       }
    
       private final List<MyDispatcherEntry> entries = new LinkedList<>();
    
       public void enqueue(String path, MockResponse response) {
          enqueue(req -> req.getPath().equals(path), req -> response);
       }
    
       public void enqueue(
          Predicate<RecordedRequest> predicate, 
          Function<RecordedRequest, MockResponse> action
       ) {
          entries.add(new MyDispatcherEntry(predicate, action));
       }
    
       public MockResponse dispatch(RecordedRequest req) {
          for (Iterator<MyDispatcherEntry> it = entries.iterator(); it.hasNext(); ) {
             MyDispatcherEntry entry = it.next();
             if (entry.predicate.test(req)) {
                it.remove(); // discard this action so another can match next time
                return entry.action.apply(req);
             }
          }
          throw new RuntimeException("No matching entries for " + req);
       }
    
    }
    

    Usage

    MyDispatcher dispatcher = new MyDispatcher();
    dispatcher.enqueue("/query1", new MockResponse().setResponseCode(200).setBody(...));
    dispatcher.enqueue("/query2", new MockResponse().setResponseCode(200).setBody(...));
    dispatcher.enqueue("/query3", new MockResponse().setResponseCode(502));
    dispatcher.enqueue("/query3", new MockResponse().setResponseCode(502));
    dispatcher.enqueue("/query3", new MockResponse().setResponseCode(200).setBody(...));