Search code examples
javaservletsjunitaemjunit5

Error while executing AEM sling servlet junit5 test case


I have written below AEM servlet and junit5 test case but I am getting error while executing junit5 test case

import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import javax.servlet.Servlet;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.TimeUnit;

@Component(service = { Servlet.class })
@SlingServletResourceTypes(
    resourceTypes = { "sample-aem/components/content/profile" },
    methods = { "GET" },
    extensions = { "json" })
@Designate(ocd = TestServlet.Configuration.class)
public class TestServlet extends SlingSafeMethodsServlet {

    private Configuration configuration;
    private PoolingHttpClientConnectionManager poolingHttpClientConnectionManager;
    protected CloseableHttpClient closeableHttpClient;

    @Activate
    void activate(final Configuration config) {
        this.configuration = config;
        poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
        poolingHttpClientConnectionManager.setMaxTotal(35);
        poolingHttpClientConnectionManager.setDefaultMaxPerRoute(2);
    }


    @Override
    protected void doGet(SlingHttpServletRequest request,SlingHttpServletResponse response) throws IOException {
        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");
        HttpGet httpGet = null;
        URI uri = null;
        String finalResponse = null;
        try {
            closeableHttpClient = HttpClients.custom().setConnectionManager(poolingHttpClientConnectionManager).build();
            uri = new URIBuilder(configuration.baseUrl()+configuration.userid()).build();
            httpGet = new HttpGet(uri);
            finalResponse = getCloseableHttpClient().execute(httpGet, new CustomResponseHandler());
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (httpGet != null) {
                httpGet.releaseConnection();
            }
        }
        response.getWriter().write(finalResponse);
    }

    private CloseableHttpClient getCloseableHttpClient() {
        this.poolingHttpClientConnectionManager.closeExpiredConnections();
        this.poolingHttpClientConnectionManager.closeIdleConnections(5, TimeUnit.MINUTES);
        return closeableHttpClient;
    }

    @ObjectClassDefinition
    public @interface Configuration {
        @AttributeDefinition(
            name = "Base Url",
            description = "API server base url"
        )
        String baseUrl() default "https://api.baseurl.com/";
        @AttributeDefinition(
            name = "userid",
            description = "Unique user id."
        )
        String userid() default "12345";
    }
}

Below is the helper class

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CustomResponseHandler implements ResponseHandler<String> {

    @Override
    public String handleResponse(HttpResponse response) throws IOException {
        try {
            StatusLine statusLine = response.getStatusLine();
            int statusCode = statusLine.getStatusCode();
            if (statusCode == 201) {
                String entityId = response.getFirstHeader("uniqueid").getValue();
                Pattern pattern = Pattern.compile("\\((.*)\\)");
                Matcher m = pattern.matcher(entityId);
                if (m.find()) {
                    return m.group(1);
                } else {
                    return entityId;
                }
            } else {
                HttpEntity entity = response.getEntity();
                String body = entity != null ? EntityUtils.toString(entity) : null;
                return body;
            }
        } finally {
            ((CloseableHttpResponse) response).close();
        }
    }
}

I have written Sling Servlet junit5 testcase and added all required dependency in POM file and my junit5 test are working but below is the Junit5 test class throwing error while executing. Seems I have done something wrong while mocking the object


import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicStatusLine;
import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ExtendWith(AemContextExtension.class)
class ProfileServletTest {

    AemContext aemContext = new AemContext();
    ProfileServlet profileServlet = new ProfileServlet();
    MockSlingHttpServletRequest request;
    MockSlingHttpServletResponse response;

    @Mock
    private PoolingHttpClientConnectionManager cm;
    @Mock
    private CloseableHttpClient closeableHttpClient;
    @Mock
    CloseableHttpResponse closeableHttpResponse;
    @Mock
    private HttpClients httpClients;
    
    @BeforeEach
    void setUp() throws IOException {
        aemContext.build().resource("/content/sample-aem/profile","jcr:title","Profile");
        aemContext.currentResource("/content/sample-aem/profile");
        aemContext.requestPathInfo().setExtension("json");
        ProfileServlet.Configuration configuration = mock(ProfileServlet.Configuration.class);
        when(configuration.baseUrl()).thenReturn("https://api.baseurl.com/");
        when(configuration.userid()).thenReturn("12345");
        when(httpClients.custom().setConnectionManager(cm).build()).thenReturn(closeableHttpClient);
        when(closeableHttpClient.execute(any())).thenReturn(closeableHttpResponse);
        when(closeableHttpResponse.getEntity()).thenReturn(new StringEntity("{'result':'ok'}"));
        when(closeableHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("http",1,1),200,"OK"));
        profileServlet.activate(configuration);
        request = aemContext.request();
        response = aemContext.response();
    }

    @Test
    void doGet() throws IOException {
        profileServlet.doGet(request,response);
        assertEquals("{'result':'ok'}",response.getOutputAsString());
    }
}

getting below error

org.mockito.exceptions.misusing.MissingMethodInvocationException: 
when() requires an argument which has to be 'a method call on a mock'.
For example:
    when(mock.getArticles()).thenReturn(articles);

Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
   Those methods *cannot* be stubbed/verified.
   Mocking methods declared on non-public parent classes is not supported.
2. inside when() you don't call method on mock but on some other object.


    at aem.core.servlets.ProfileServletTest.setUp(ProfileServletTest.java:52)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:686)

Solution

  • The mocking of the http-client building had 2 main issues:

    1. All steps of the builder-chain have to be mocked (return self)
    2. The static method HttpClients.custom() is harder to mock with Mockito

    Here is a working version of your JUnit Test, with some comments:

    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.mockito.ArgumentMatchers.any;
    import static org.mockito.Mockito.when;
    
    import io.wcm.testing.mock.aem.junit5.AemContext;
    import io.wcm.testing.mock.aem.junit5.AemContextExtension;
    import java.io.IOException;
    import org.apache.http.client.ResponseHandler;
    import org.apache.http.client.methods.HttpGet;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClientBuilder;
    import org.apache.http.impl.client.HttpClients;
    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.MockedStatic;
    import org.mockito.Mockito;
    import org.mockito.junit.jupiter.MockitoExtension;
    
    @ExtendWith(AemContextExtension.class)
    @ExtendWith(MockitoExtension.class)
    class ProfileServletTest {
    
      private final AemContext aemContext = new AemContext();
    
      @Mock
      private HttpClientBuilder httpClientBuilder;
      @Mock
      private CloseableHttpClient closeableHttpClient;
    
      private ProfileServlet profileServlet;
    
      @BeforeEach
      void setUp() throws IOException {
        aemContext.build().resource("/content/sample-aem/profile", "jcr:title", "Profile");
        aemContext.currentResource("/content/sample-aem/profile");
        aemContext.requestPathInfo().setExtension("json");
    
        // Mock every step of the builder-chain
        when(httpClientBuilder.setConnectionManager(any())).thenReturn(httpClientBuilder);
        when(httpClientBuilder.build()).thenReturn(closeableHttpClient);
    
        // execute() already returns a String
        when(closeableHttpClient.execute(any(HttpGet.class), any(ResponseHandler.class)))
            .thenReturn("{'result':'ok'}");
    
        // use AEM-Mockups to register/inject/activate services
        this.profileServlet = aemContext.registerInjectActivateService(ProfileServlet.class,
            "baseUrl", "https://api.baseurl.com/", "userid", "12345");
      }
    
      @Test
      void doGet() throws IOException {
        // Static methods are NOT easy to mock
        try (MockedStatic<HttpClients> httpClientsMock = Mockito.mockStatic(HttpClients.class)) {
          httpClientsMock.when(HttpClients::custom).thenReturn(httpClientBuilder);
    
          profileServlet.doGet(aemContext.request(), aemContext.response());
          assertEquals("{'result':'ok'}", aemContext.response().getOutputAsString());
        }
      }
    }