Route Dispatcher for MockWebServer
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.
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?
That is a pity, MockWebServer, in my opinion, is by far more convenient and it does not have tons of dependencies.
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!
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.