square / okhttp

Square’s meticulous HTTP client for the JVM, Android, and GraalVM.
https://square.github.io/okhttp/
Apache License 2.0
45.71k stars 9.15k forks source link

Route Dispatcher for MockWebServer #8115

Open victorherraiz opened 9 months ago

victorherraiz commented 9 months ago

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.

yschimke commented 9 months ago

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?

victorherraiz commented 9 months ago

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

swankjesse commented 9 months ago

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!

victorherraiz commented 9 months ago

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.