Unfortunately when building AEM backend logic, the org.apache.http.client.HttpClient is non-trivial to write unit tests. In my case, I scavenged across the net to find a solution where you can make an GET and POST request via HttpClient in particular for AEM, and unit test is performing as expected. In order for me write successful to my servlet, I had to refactor the code. Forcefully, I had to change the org.apache.http.client.HttpClient to org.apache.http.osgi.services.HttpClientBuilderFactory OSGI service; the HttpClientBuilderFactor giving me the HttpClient that I need.
This article will showcase a successful unit test with the doGet and doPost example code. The result of the methods will return a JSON string, which we will mock.
1. Dependencies
Before we begin, I would like to mention the dependencies that I am using to get this code to work (please make sure you are using JUNIT5):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <dependency> <groupId>io.wcm</groupId> <artifactId>io.wcm.testing.aem-mock.junit5</artifactId> <exclusions> <exclusion> <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.models.impl</artifactId> </exclusion> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> </exclusion> </exclusions> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>4.1.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>4.1.0</version> <scope>test</scope> </dependency> |
2. HttpClientServlet.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | package com.sourcedcode.core.servlets; import com.adobe.granite.rest.Constants; import com.drew.lang.annotations.NotNull; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; import com.google.gson.JsonObject; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.osgi.services.HttpClientBuilderFactory; 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.SlingAllMethodsServlet; import org.apache.sling.servlets.annotations.SlingServletResourceTypes; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import javax.servlet.Servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @Component(service = Servlet.class) @SlingServletResourceTypes( methods = {HttpConstants.METHOD_GET, HttpConstants.METHOD_POST}, resourceTypes = "sourcedcode/component", selectors = {"example"}, extensions = {"json"} ) public class HttpClientServlet extends SlingAllMethodsServlet { @Reference private HttpClientBuilderFactory clientBuilderFactory; @Override protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response) throws ServletException, IOException { CloseableHttpClient client = clientBuilderFactory.newBuilder().build(); HttpGet getMethod = new HttpGet("https://dummy.restapiexample.com/api/v1/employee/1"); CloseableHttpResponse httpClientResponse = client.execute(getMethod); String responseBody = EntityUtils.toString(httpClientResponse.getEntity()); JsonObject jsonObject = new Gson().fromJson(responseBody, JsonObject.class); response.setStatus(HttpServletResponse.SC_OK); response.setContentType(Constants.CT_JSON); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.getWriter().write(jsonObject.toString()); } @Override protected void doPost(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response) throws ServletException, IOException { CloseableHttpClient client = clientBuilderFactory.newBuilder().build(); ObjectMapper mapper = new ObjectMapper(); Map<String, String> object = new HashMap<>(); object.put("test", "value"); StringEntity requestData = new StringEntity( mapper.writeValueAsString(object), ContentType.APPLICATION_JSON); HttpPost postMethod = new HttpPost("https://freemeposting.free.beeceptor.com/my/api/path"); postMethod.setEntity(requestData); postMethod.setHeader("Test", "Test"); CloseableHttpResponse preRenderedResponse = client.execute(postMethod); String responseBody = EntityUtils.toString(preRenderedResponse.getEntity()); JsonObject jsonObject = new Gson().fromJson(responseBody, JsonObject.class); response.setStatus(HttpServletResponse.SC_OK); response.setContentType(Constants.CT_JSON); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.getWriter().write(jsonObject.toString()); } } |
3. HttpClientServletTest.java
In all, the most important line here that you should take a look at is line:60. On this line you can see that I am injecting mocks into the HttpClientBuilderFactory osgi service, which when clientBuilderFactory.newBuilder().build() is being called, the HttpClientBuilderFactory is bring returned in lin:47, which will be the HttpClient itself.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | package com.sourcedcode.core.servlets; import io.wcm.testing.mock.aem.junit5.AemContext; import io.wcm.testing.mock.aem.junit5.AemContextExtension; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.osgi.services.HttpClientBuilderFactory; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.servlets.SlingAllMethodsServlet; import org.apache.sling.testing.mock.sling.ResourceResolverType; import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import javax.servlet.ServletException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith({AemContextExtension.class, MockitoExtension.class}) public class HttpClientServletTest extends SlingAllMethodsServlet { private final AemContext aemContext = new AemContext(ResourceResolverType.JCR_MOCK); @Mock HttpClientBuilderFactory clientBuilderFactory; @Mock CloseableHttpClient closeableHttpClientMock; @Mock CloseableHttpResponse closeableHttpResponseMock; @InjectMocks private HttpClientServlet unitTest = new HttpClientServlet(); @Test public void testServletDoGet() throws ServletException, IOException { String responseData = "{status: "success"}"; InputStream anyInputStream = new ByteArrayInputStream(responseData.getBytes()); MockSlingHttpServletRequest request = aemContext.request(); SlingHttpServletResponse response = mock(SlingHttpServletResponse.class); HttpEntity httpEntityMock = mock(HttpEntity.class); when(clientBuilderFactory.newBuilder()).thenReturn(mock(HttpClientBuilder.class)); when(clientBuilderFactory.newBuilder().build()).thenReturn(closeableHttpClientMock); when(closeableHttpClientMock.execute(any())).thenReturn(closeableHttpResponseMock); when(closeableHttpResponseMock.getEntity()).thenReturn(httpEntityMock); when(httpEntityMock.getContent()).thenReturn(anyInputStream); PrintWriter printWriterMock = mock(PrintWriter.class); when(response.getWriter()).thenReturn(printWriterMock); aemContext.registerService(HttpClientBuilderFactory.class, clientBuilderFactory); unitTest.doGet(request, response); verify(printWriterMock).write(eq("{"status":"success"}")); } @Test public void testServletDoPost() throws ServletException, IOException { String responseData = "{status: "success"}"; InputStream anyInputStream = new ByteArrayInputStream(responseData.getBytes()); MockSlingHttpServletRequest request = aemContext.request(); SlingHttpServletResponse response = mock(SlingHttpServletResponse.class); HttpEntity httpEntityMock = mock(HttpEntity.class); when(clientBuilderFactory.newBuilder()).thenReturn(mock(HttpClientBuilder.class)); when(clientBuilderFactory.newBuilder().build()).thenReturn(closeableHttpClientMock); when(closeableHttpClientMock.execute(any())).thenReturn(closeableHttpResponseMock); when(closeableHttpResponseMock.getEntity()).thenReturn(httpEntityMock); when(httpEntityMock.getContent()).thenReturn(anyInputStream); PrintWriter printWriterMock = mock(PrintWriter.class); when(response.getWriter()).thenReturn(printWriterMock); aemContext.registerService(HttpClientBuilderFactory.class, clientBuilderFactory); unitTest.doPost(request, response); verify(printWriterMock).write(eq("{"status":"success"}")); } } |
Wow, thank you for this; I was stuck on this problem for a long time!
You’re welcome
Thank you Bri!
So bizarre, but thanks!