okhttp icon indicating copy to clipboard operation
okhttp copied to clipboard

Route Dispatcher for MockWebServer

Open victorherraiz opened this issue 2 years ago • 4 comments

I would like to have an alternative to sequential mocking and verifications. Some applications could perform calls concurrently or in a non predictable order. Or it just seems that we are testing "implementation details" when we "force" the order in the interaction. There are several solutions to this issue, like having multiple MockWebServer instances but they feel rather comvoluted.

I implemented a draft of a Route dispacher:

public class RouteDispatcher extends Dispatcher {

    private final List<Route> routes;
    private final List<IdAndRequest> requests =
        Collections.synchronizedList(new ArrayList<>());
    private final MockResponse defaultResponse;

    private RouteDispatcher(Builder builder) {
        this.routes = builder.routes;
        this.defaultResponse = builder.defaultResponse;
    }

    private record IdAndRequest(Object id, RecordedRequest request) {
    }

    @NotNull
    @Override
    public MockResponse dispatch(@NotNull RecordedRequest req) {
        for (var route : routes) {
            if (route.matcher.test(req)) {
                requests.add(new IdAndRequest(route.id, req));
                return route.response;
            }
        }
        return defaultResponse;
    }

    @FunctionalInterface
    public interface RequestMatcher extends Predicate<RecordedRequest> {
    }

    public static RequestMatcher pathStartsWith(String path) {
        return req -> {
            var reqPath = req.getPath();
            return reqPath != null && reqPath.startsWith(path);
        };
    }

    private record Route(
        Object id,
        RequestMatcher matcher,
        MockResponse response) {
    }

    public List<RecordedRequest> getRequests(Object id) {
        return requests.stream()
            .filter(r -> r.id().equals(id))
            .map(IdAndRequest::request)
            .toList();
    }

    public RecordedRequest getRequest(Object id) {
       var requests = getRequests(id);
       assertEquals("Expected exactly one request with id " + id, 1, requests.size());
       return requests.get(0);
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private final List<Route> routes = new ArrayList<>();
        public MockResponse defaultResponse = new MockResponse().setResponseCode(404);

        private Builder() {
            // Use the method builder() instead
        }

        public Builder addRoute(Object id, RequestMatcher matcher, MockResponse response) {
            routes.add(new Route(id, matcher, response));
            return this;
        }

        public Builder addRoute(Object id, RequestMatcher matcher, UnaryOperator<MockResponse> response) {
            return addRoute(id, matcher, response.apply(new MockResponse()));
        }

        public Builder addRoute(RequestMatcher matcher, MockResponse response) {
            routes.add(new Route(matcher, matcher, response));
            return this;
        }

        public Builder addRoute(RequestMatcher matcher, UnaryOperator<MockResponse> response) {
            return addRoute(matcher, response.apply(new MockResponse()));
        }

        public RouteDispatcher build() {
            return new RouteDispatcher(this);
        }

        public RouteDispatcher buildAndSet(MockWebServer server) {
            var dispatcher = this.build();
            server.setDispatcher(dispatcher);
            return dispatcher;
        }

    }
}

And some example of the usage:

    @Test
    void interactions_tests(@Autowired WebClient.Builder builder) {
        var path01 = pathStartsWith("/path01");
        var dispatcher = RouteDispatcher.builder()
            .addRoute(path01, res -> res.setResponseCode(200))
            .buildAndSet(server);
       // Emulation of real service call
        var response = builder.build().get().uri("http://localhost:8090/path01")
            .retrieve().toBodilessEntity().block();
        assertNotNull(response);
        assertEquals(HttpStatus.OK, response.getStatusCode());

        var request = dispatcher.getRequest(path01);
        assertEquals("/path01", request.getPath());
    }

I like this kind of feature in Wiremock but I preffer your implementation with less dependencies and included the Spring Boot BOM.

If you feel this useful, I could code that in kotlin, add the proper test and do a pull request after your evaluation.

victorherraiz avatar Nov 26 '23 10:11 victorherraiz

We aren't very actively improving MockWebServer. If you find features that you need in Wiremock, it's probably better to go with that rather than going through the ringer of a big feature change to MockWebServer.

cc @swankjesse thoughts?

yschimke avatar Nov 26 '23 18:11 yschimke

That is a pity, MockWebServer, in my opinion, is by far more convenient and it does not have tons of dependencies.

victorherraiz avatar Nov 26 '23 18:11 victorherraiz

I think this RouteDispatcher thing is great! But I’d prefer to omit it from the MockWebServer library. If you’d like to do a new library, please do!

swankjesse avatar Dec 03 '23 21:12 swankjesse

OK. At the moment I am going to keep this at a shared library for test in my company, if you reconsider adding a route dispacher, let me know. I could do a pull request with tested RouteDispacher.

victorherraiz avatar Dec 07 '23 07:12 victorherraiz