Let’s look at an example servlet that proxies to a hard-coded url.
// VERY dudamentary code to illustrate the point
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.ServletResolverConstants;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
@Component(
service = Servlet.class,
property = {
Constants.SERVICE_DESCRIPTION + "=Search Proxy servlet",
ServletResolverConstants.SLING_SERVLET_METHODS + "=" + HttpConstants.METHOD_GET,
ServletResolverConstants.SLING_SERVLET_RESOURCE_TYPES + "="
+ SearchProxyServlet.RESOURCE_TYPE,
ServletResolverConstants.SLING_SERVLET_EXTENSIONS + "=json",
ServletResolverConstants.SLING_SERVLET_SELECTORS + "=searchproxy"
})
public class SearchProxyServlet extends SlingSafeMethodsServlet {
public static final String RESOURCE_TYPE = "some/resource/type";
@Override
protected void doGet(SlingHttpServletRequest slingRequest, SlingHttpServletResponse slingResponse)
throws ServletException, IOException {
// prepare credentials
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(
new AuthScope("search.com", 443),
new UsernamePasswordCredentials("test", "test"));
slingResponse.setContentType("application/json");
try (CloseableHttpClient httpClient =
HttpClients.custom().setDefaultCredentialsProvider(credentialsProvider).build()) {
HttpGet httpGet = new HttpGet(new URI("https://search.com/endpoint.json"));
try (CloseableHttpResponse httpResponse = httpClient.execute(httpGet)) {
slingResponse.getWriter().write(EntityUtils.toString(httpResponse.getEntity()));
}
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
}
As you can see, we are sending a request to https://search.com/endpoint.json so when you write a unit test for this and invoke doGet, a request will always be sent. But we don’t want that, we want to mock that request. You could use PowerMock, but adding that to your project introduces its own problems. PowerMock is intended for experienced developers and excessive use of it may be an indication of bad implementation/architecture.
A better implementation using an OSGI service
We can move the httpClient to its own OSGI service:
package com.ahmedmusallam.service;
import java.io.IOException;
import java.net.URI;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
@Component(
immediate = true,
property = {"label=Http Client Service", "description=A service for making HTTP calls"},
service = HttpClientService.class)
public class HttpClientService {
/** Perform a get request with the provided credentials provider. */
public String doGet(URI uri, CredentialsProvider credentialsProvider) throws IOException {
if (credentialsProvider == null || uri == null) {
return null;
}
try (CloseableHttpClient httpClient =
HttpClients.custom().setDefaultCredentialsProvider(credentialsProvider).build()) {
HttpGet httpGet = new HttpGet(uri);
try (CloseableHttpResponse httpResponse = httpClient.execute(httpGet)) {
return EntityUtils.toString(httpResponse.getEntity());
}
}
}
}
This makes it easier to mock the service or provide our own implementation of it in our test class.
An improved implementation of the proxy servlet:
// Agian, crude impl to illustrate the point
import com.ahmedmusallam.service.HttpClientService;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.ServletResolverConstants;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@Component(
service = Servlet.class,
property = {
Constants.SERVICE_DESCRIPTION + "=Search Proxy servlet",
ServletResolverConstants.SLING_SERVLET_METHODS + "=" + HttpConstants.METHOD_GET,
ServletResolverConstants.SLING_SERVLET_RESOURCE_TYPES + "="
+ SearchProxyServlet.RESOURCE_TYPE,
ServletResolverConstants.SLING_SERVLET_EXTENSIONS + "=json",
ServletResolverConstants.SLING_SERVLET_SELECTORS + "=searchproxy"
})
public class SearchProxyServlet extends SlingSafeMethodsServlet {
public static final String RESOURCE_TYPE = "some/resource/type";
private HttpClientService httpClientService;
@Reference
public void setHttpClientService(HttpClientService httpClientService) {
this.httpClientService = httpClientService;
}
@Override
protected void doGet(SlingHttpServletRequest slingRequest, SlingHttpServletResponse slingResponse)
throws ServletException, IOException {
// prepare credentials
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(
new AuthScope("test.com", 443),
new UsernamePasswordCredentials("test", "test"));
slingResponse.setContentType("application/json");
try {
String response = httpClientService.doGet(new URI("https://test.com/endpoint.json"), credentialsProvider);
slingResponse.getWriter().write(response);
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
}
Now, this is all good, and we can mock the httpClientService and return a specific string, here is an example test:
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import com.ahmedmusallam.service.HttpClientService;
import com.ahmedmusallam.utils.AppAemContext;
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import java.io.IOException;
import java.net.URI;
import javax.servlet.ServletException;
import org.apache.http.client.CredentialsProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith({AemContextExtension.class, MockitoExtension.class})
class SearchProxyServletTest {
public final AemContext context = AppAemContext.newAemContext();
@Mock
HttpClientService httpClientService = new HttpClientService();
SearchProxyServlet searchProxyServlet = new SearchProxyServlet();
@BeforeEach
void beforeEach() throws IOException {
when(httpClientService.doGet(any(URI.class), any(CredentialsProvider.class))).thenReturn("{}");
searchProxyServlet.setHttpClientService(httpClientService);
}
@Test
void doGet() throws ServletException, IOException {
// cover case where query
searchProxyServlet.doGet(context.request(), context.response());
assertEquals("{}", context.response().getOutputAsString());
}
}
Testing the HttpClientService
All good so far! But what about testing HttpClientService itself? For that, we would need an HTTP server to run before the test class runs and stop right after. I have found a jUnit4 @Rule for such server here: https://gist.github.com/rponte/710d65dc3beb28d97655. However, I’m using jUnit 5. So I’ve converted that rule into a jUnit5 Extension and here it is:
You can also see it in this gist
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.http.client.utils.URIBuilder;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
/*
* Note: I chose to implement `BeforeAllCallback` and AfterAllCallback
* but not `AfterEachCallback` and `BeforeEachCallback` for performance reasons.
* I wanted to only run one server per test class and I can register handlers
* on a per-test-method basis. You could implement the `BeforeEachCallback` and `AfterEachCallback`
* interfaces if you really need that behavior.
*/
public class HttpServerExtension implements BeforeAllCallback, AfterAllCallback {
public static final int PORT = 6991;
public static final String HOST = "localhost";
public static final String SCHEME = "http";
private com.sun.net.httpserver.HttpServer server;
@Override
public void afterAll(ExtensionContext extensionContext) throws Exception {
if (server != null) {
server.stop(0); // doesn't wait all current exchange handlers complete
}
}
@Override
public void beforeAll(ExtensionContext extensionContext) throws Exception {
server = HttpServer.create(new InetSocketAddress(PORT), 0);
server.setExecutor(null); // creates a default executor
server.start();
}
public static URI getUriFor(String path) throws URISyntaxException{
return new URIBuilder()
.setScheme(SCHEME)
.setHost(HOST)
.setPort(PORT)
.setPath(path)
.build();
}
public void registerHandler(String uriToHandle, HttpHandler httpHandler) {
server.createContext(uriToHandle, httpHandler);
}
}
As you can see, I run an HTTP server before a test class is run, and stop the server after the test class is run.
and this is the code for a JsonSuccessHandler:
You could, of course, write your own simple handler for other types of requests.
package com.ahmedmusallam.extension;
import java.io.IOException;
import java.nio.charset.Charset;
import org.apache.commons.io.IOUtils;
import java.net.HttpURLConnection;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
// credit: https://gist.github.com/rponte/710d65dc3beb28d97655#file-httpserverrule-java
public class JsonSuccessHandler implements HttpHandler {
private String responseBody;
private static final String contentType = "application/json";
public JsonSuccessHandler() {}
public JsonSuccessHandler(String responseBody) {
this.responseBody = responseBody;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
exchange.getResponseHeaders().add("Content-Type", contentType);
exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, responseBody.length());
IOUtils.write(responseBody, exchange.getResponseBody(), Charset.defaultCharset());
exchange.close();
}
}
Now, let’s write the unit test for our HttpClientService:
package com.ahmedmusallam.service;
import static org.junit.jupiter.api.Assertions.*;
import com.ahmedmusallam.extension.HttpServerExtension;
import com.ahmedmusallam.extension.JsonSuccessHandler;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class HttpClientServiceTest {
private HttpClientService httpClientService = new HttpClientService();
@RegisterExtension // MUST be static, see: https://junit.org/junit5/docs/current/user-guide/#extensions-registration-programmatic-static-fields
static HttpServerExtension httpServerExtension = new HttpServerExtension();
@Mock
CredentialsProvider credentialsProvider;
@Test
void doGet() throws IOException, URISyntaxException {
assertNull(httpClientService.doGet(null, credentialsProvider));
assertNull(httpClientService.doGet(new URIBuilder().build(), null));
httpServerExtension.registerHandler("/test", new JsonSuccessHandler("{}"));
URI uri = HttpServerExtension.getUriFor("/test");
assertEquals("{}", httpClientService.doGet(uri, new BasicCredentialsProvider()));
}
}
As you can see, I’ve created an HttpServerExtension and registered a handler for path /test with an expected result, then ran my service’s doGet method against that handler and verified the output.
That’s it! You can add more methods to send POST requests and other types of requests to the HttpClientService and test those in the same fashion.
No comments:
Post a Comment
If you have any doubts or questions, please let us know.