Force Jersey to read mocks from JerseyTest
Asked Answered
C

4

8

I want to test a Resourse with JerseyTest. I have created the following test:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:testApplicationContext.xml")
public class ResourceTest extends JerseyTest
{
    @Configuration
    public static class Config
    {
        @Bean
        public AObject aObject()
        {
            return mock(AObject.class);
        }
    }

    @Autowired
    public AObject _aObject;

    @Test
    public void testResource()
    {
        // configouring mock _aObject

        Response response = target("path");
        Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
    }


    @Override
    protected Application configure()
    {
        return new ResourceConfig(Resource.class).property("contextConfigLocation", "classpath:testApplicationContext.xml");
    }
}

My Resource also has an AObject reference with @Autowired annotation.

My problem is that my JerseyTest and the Resource (that is configured by the test) have different instances for the Mock object. In the console I see that the testApplicationContext.xml is loaded twice, once for the test and one for the Resource.

How can I force jersey to use the same mock?

Cohesive answered 1/7, 2014 at 11:59 Comment(0)
I
18

After debugging the jersey-spring3 (version 2.9.1) library it seems that the problem lies in the SpringComponentProvider.createSpringContext

private ApplicationContext createSpringContext() {
    ApplicationHandler applicationHandler = locator.getService(ApplicationHandler.class);
    ApplicationContext springContext = (ApplicationContext) applicationHandler.getConfiguration().getProperty(PARAM_SPRING_CONTEXT);
    if (springContext == null) {
        String contextConfigLocation = (String) applicationHandler.getConfiguration().getProperty(PARAM_CONTEXT_CONFIG_LOCATION);
        springContext = createXmlSpringConfiguration(contextConfigLocation);
    }
    return springContext;
}

It checks if a property named "contextConfig" exists in the application properties and if not it initializes the spring application context. Even if you initialized a spring application context in your tests, jersey will create another context and use that one instead. So we have to somehow pass the ApplicationContext from our tests in the Jersey Application class. The solution is the following:

@ContextConfiguration(locations = "classpath:jersey-spring-applicationContext.xml")
public abstract class JerseySpringTest
{
    private JerseyTest _jerseyTest;

    public final WebTarget target(final String path)
    {
        return _jerseyTest.target(path);
    }

    @Before
    public void setup() throws Exception
    {
        _jerseyTest.setUp();
    }

    @After
    public void tearDown() throws Exception
    {
        _jerseyTest.tearDown();
    }

    @Autowired
    public void setApplicationContext(final ApplicationContext context)
    {
        _jerseyTest = new JerseyTest()
        {
            @Override
            protected Application configure()
            {
                ResourceConfig application = JerseySpringTest.this.configure();
                application.property("contextConfig", context);

                return application;
            }
        };
    }

    protected abstract ResourceConfig configure();
}

The above class will take the application context from our tests and pass it to the configured ResourceConfig, so that the SpringComponentProvider will return the same application context to jersey. We also use the jersey-spring-applicationContext.xml in order to include jersey specific spring configuration.

We cannot inherit from JerseyTest because it initializes the Application in the constructor before the test application context is initialized.

You can now use this base class to create your tests for example

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:testContext.xml")
public class SomeTest extends JerseySpringTest
{
     @Autowired
     private AObject _aObject;

     @Test
     public void test()
     {
          // configure mock _aObject when(_aObject.method()).thenReturn() etc...

         Response response = target("api/method").request(MediaType.APPLICATION_JSON).get();
         Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
     }

     @Override
     protected ResourceConfig configure()
     {
        return new ResourceConfig(MyResource.class);
     }
}

In testContext.xml add the following definition in order to inject a mock AObject.

<bean class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="com.yourcompany.AObject" />
</bean>
Irrelevance answered 1/7, 2014 at 14:24 Comment(1)
Thank you very much for this answer it solves my problems to develop integration tests with the jersey test framework and spring-test. In particular my problem was to instruct mock objects during test execution in the same ApplicationContext. Thanks for all,Nephralgia
B
1

I couldn't get the answer https://mcmap.net/q/1260291/-force-jersey-to-read-mocks-from-jerseytest from @Grigoris working, although his explanation for why it is happening is correct.

In the end I went for the approach below which exposes a special setter to insert the mock object. Not as 'clean' as the approach above, but worth the tradeoff of exposing the apiProvider I wanted to mock so I could write some tests..

public MyAPITest extends JerseyTest {

    // Declare instance of the API I want to test - this will be instantiated in configure()
    MyAPI myAPI;

    @Override
    protected ResourceConfig configure()
    {  
        MockitoAnnotations.initMocks(this);
        myAPI = new MyAPI();

        ResourceConfig resourceConfig = new ResourceConfig();
        resourceConfig.register(MyAPI).property("contextConfig", new ClassPathXmlApplicationContext("classpath:spring.testHarnessContext.xml"));
        return resourceConfig;
    }

    @Mock
    private MyAPIProvider mockAPIProvider;

    @Before
    public void before() {
        myAPI.setMockProvider(mockAPIProvider);
    }


    @Test
    public void test() {

        // I can now define the mock behaviours and call the API and validate the outcomes
        when(mockAPIProvider....)
        target().path("....)            
    }
}
Bungalow answered 14/11, 2016 at 14:29 Comment(0)
S
0

If anyone is interested in the solution https://mcmap.net/q/1260291/-force-jersey-to-read-mocks-from-jerseytest from Kevin for Jersey v1:

public MyAPITest extends JerseyTest {

    @InjectMocks
    MyAPI myAPI;

    @Mock
    MyApiService myApiService;

    @Override
    protected AppDescriptorconfigure()
    {  
        MockitoAnnotations.initMocks(this);

        ResourceConfig rc = new DefaultResourceConfig();
        rc.getSingletons().add(myAPI);

        return new LowLevelAppDescriptor.Builder(rc).contextPath("context").build();
    }

    @Test
    public void test() {
        // I can now define the mock behaviours
        when(myApiService...)

        WebResource webResource = resource().path("mypath");
        ClientResponse result = webResource.get(ClientResponse.class);            
    }
}
Screwed answered 22/2, 2018 at 8:5 Comment(3)
I'm trying to achieve what you did, but your code seems faulty. For instance you don't have a return value for AppDescriptorconfigure yet you return something in the method body. Also it shouldn't start with a capital A. Tried searching for this method in the JerseyTest source code and also couldn't find anything.Hussar
@Nom1fan, thank for the hint. If you achieve to make this sample code run, feel free to correct it. Unfortunately, I don't have access to the code, where I used it.Screwed
I was actually able to make it work without any additional code. I simply used @Mock and @InejctMocks like you did, and in JerseyTest's configure() method I added MockitoAnnotations.openMocks(this);. Mocks were injected successfully.Hussar
N
0

Further improvising the accepted solution by removing xml dependency. More details available here.

JerseySpringTest abstracting JerseyTest:

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.glassfish.jersey.logging.LoggingFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.jersey.test.TestProperties;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

/** Run JerseyTest with custom spring context with mocks Mimics Spring @WebMvcTest with @MockBean */
public abstract class JerseySpringTest extends JerseyTest {

  @Override
  protected ResourceConfig configure() {
    MockitoAnnotations.openMocks(this);
    enable(TestProperties.LOG_TRAFFIC);
    enable(TestProperties.DUMP_ENTITY);
    set(TestProperties.CONTAINER_PORT, "0");
    final ResourceConfig resourceConfig =
        new ResourceConfig()
            .property("contextConfig", createSpringContext(getBeanMap()))
            .property(LoggingFeature.LOGGING_FEATURE_LOGGER_LEVEL_SERVER, "WARNING")
            .register(getResourceClass());
    return serverConfig(resourceConfig);
  }

  /**
   * Gives the test class opportunity to further customize the configuration. Like registering a
   * MultiPartFeature if required.
   *
   * @param config
   * @return
   */
  protected ResourceConfig serverConfig(final ResourceConfig config) {
    return config;
  }

  /**
   * Supplies all the bean objects required to be loaded in the application context for the Resource class
   * under test
   *
   * @return
   */
  protected List<Object> getBeans() {
    return Collections.emptyList();
  }
  
   /**
   * Supplies all the bean objects with name qualifier required to be loaded in the application context for the Resource class
   * under test
   *
   * @return
   */
  protected Map<String, Object> getQualifiedBeans() {
    return Collections.emptyMap();
  }

  private Map<String, Object> getBeanMap() {
    final Map<String, Object> result = new HashMap<>();
    CollectionUtils.emptyIfNull(getBeans())
        .forEach(obj -> result.put(StringUtils.uncapitalize(obj.getClass().getSimpleName()), obj));
    result.putAll(MapUtils.emptyIfNull(getQualifiedBeans()));
    return result;
  }

  /**
   * Resource class under test
   *
   * @return
   */
  protected abstract Class<?> getResourceClass();

  /**
   * Creates & returns a Spring GenericApplicationContext from the given beans with qualified names
   *
   * @param beans
   * @return
   */
  public static ApplicationContext createSpringContext(Map<String, Object> beans) {
    final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
    MapUtils.emptyIfNull(beans).forEach((k, obj) -> beanFactory.registerSingleton(k, obj));
    final GenericApplicationContext context = new GenericApplicationContext(beanFactory);
    context.refresh();
    return context;
  }
}

Sample Resource With Test:

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import java.util.List;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import lombok.RequiredArgsConstructor;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

@Path("/rest")
@Component
@RequiredArgsConstructor
class RestResource {
  private final ServiceXYZ serviceXYZ;
  private final ServiceABC serviceABC;
  @Qualifier("greeter")
  private final String greeterName;

  @GET
  @Path("/serviceXYZ/greet/{name}")
  public Response greetByServiceXYZ(@PathParam("name") final String name) {
    return Response.ok(serviceXYZ.greet(name) + ", Regards: " + greeterName).build();
  }

  @GET
  @Path("/serviceABC/greet/{name}")
  public Response greetByServiceABC(@PathParam("name") final String name) {
    return Response.ok(serviceABC.greet(name)+ ", Regards: " + greeterName).build();
  }
}

@Service
class ServiceXYZ {
  public final String greet(final String name) {
    return "Welcome " + name + " to Hello World!";
  }
}

@Service
class ServiceABC {
  public final String greet(final String name) {
    return "Welcome " + name + " to Hello Universe!";
  }
}

class ResourceTest extends JerseySpringTest {

  @InjectMocks private RestResource subject;
  @Mock private ServiceXYZ serviceXYZ;
  @Mock private ServiceABC serviceABC;

  // only required to override for custom server config, say if the Resource accepts file input
  @Override
  protected ResourceConfig serverConfig(final ResourceConfig config) {
    return config.register(MultiPartFeature.class);
  }

  @Override
  protected Map<String, Object> getQualifiedBeans() {
    return Map.of("greeter", "Amith Kumar");
  }

  @Override
  protected List<Object> getBeans() {
    return List.of(subject, serviceXYZ, serviceABC);
  }

  @Override
  protected Class<?> getResourceClass() {
    return RestResource.class;
  }

  // only required to override for custom client config, say if the Resource accepts file input
  @Override
  protected void configureClient(ClientConfig config) {
    config.register(MultiPartFeature.class);
  }

  @Test
  void testServiceXYZGreets() {
    // ARRANGE
    when(serviceXYZ.greet("foo")).thenReturn("Hello foo");

    // ACT
    Response output = target("/rest/serviceXYZ/greet/foo").request().get();

    // ASSERT
    assertAll(
        () -> assertEquals(Status.OK.getStatusCode(), output.getStatus()),
        () -> assertEquals("Hello foo, Regards: Amith Kumar", output.readEntity(String.class)));
  }

  @Test
  void testServiceABCGreets() {
    // ARRANGE
    when(serviceXYZ.greet("boo")).thenReturn("Hola boo");

    // ACT
    Response output = target("/rest/serviceABC/greet/boo").request().get();

    // ASSERT
    assertAll(
        () -> assertEquals(Status.OK.getStatusCode(), output.getStatus()),
        () -> assertEquals("Hola boo, Regards: Amith Kumar", output.readEntity(String.class)));
  }
}
Nauplius answered 19/8, 2022 at 6:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.