In Java, how can I mock a service loaded using ServiceLoader?
Asked Answered
E

4

13

I have a legacy Java application that has code something like this

ServiceLoader.load(SomeInterface.class)

and I want to provide a mock implementation of SomeInterface for this code to use. I use the mockito mocking framework.

Unfortunately I am unable to change the legacy code, and I do not wish to add anything statically (eg. adding things to META-INF).

Is there an easy way to do this from within the test, ie. at runtime of the test?

Ellerey answered 2/3, 2015 at 8:49 Comment(1)
are you able to change the application code just for the testing purpose?Freefloating
B
9

You can use PowerMockito along with Mockito to mock static methods:

@RunWith(PowerMockRunner.class)
@PrepareForTest(ServiceLoader.class)
public class PowerMockingStaticTest
{
    @Mock
    private ServiceLoader mockServiceLoader;

    @Before
    public void setUp()
    {
        PowerMockito.mockStatic(ServiceLoader.class);
        Mockito.when(ServiceLoader.load(Mockito.any(Class.class))).thenReturn(mockServiceLoader);
    }

    @Test
    public void test()
    {
        Assert.assertEquals(mockServiceLoader, ServiceLoader.load(Object.class));
    }
}
Benedicite answered 2/3, 2015 at 9:26 Comment(3)
This is probably the best answer, though I have had bad experiences with mocking frameworks that manipulated code in the past (eg. jMockit would cause very odd behaviour). I will probably wrap the many dozens of places this legacy code is called with an indirection that will allow me to insert a mock version of the legacy code from a test.Ellerey
@GraemeMoss: Just another idea, if the ServiceLoader-method is called at construction time, and you can get "in between" construction of the containing class & the actual usage of the loaded service class, you could probably use reflection to replace the member variables holding references to loaded service classes with references to your mocks, before they are actually used.Benedicite
I had thought of this, however the field is final. :-(Ellerey
F
7

From the ServiceLoader.load documentation:

Creates a new service loader for the given service type, using the current thread's context class loader.

So you could use a special context class loader during test runs that will dynamically generate provider-configuration files in META-INF/service. The context class loader will be used for searching for provider-configuration files due to this note in the ServiceLoader documentation:

If the class path of a class loader that is used for provider loading includes remote network URLs then those URLs will be dereferenced in the process of searching for provider-configuration files.

The context class loader needs to also load a mock implementation of the service class, which is then passed as the mock implementation.

Such a context class loader would need to do two things:

  • dynamically generating the provider configuration files on request per getResource* methods
  • dynamically generate a class (for example using ASM library) on request per loadClass methods, if it is the class that was specified in the dynamically generated provider configuration file

Using above approach, you don't need to change existing code.

Freefloating answered 2/3, 2015 at 9:11 Comment(2)
This sounds quite heavy. How would I create such a class loader?Ellerey
Interesting approach, but in practice it unfortunately breaks down. I managed to get the first part working, but in the end it has the same effect as putting a provider-configuration in Maven's src/test/resources directory. Not worth the effort. The second part is only possible with heavy bytecode manipulation, because it first loads the class and then instantiates it. But more importantly: this solution is built on assumptions about implementation details of the ServiceLoader and ClassLoader classes, which are not guaranteed to be stable across Java versions.Whipsaw
P
2

Move the call into a protected method and override it in the test. This allows you to return anything during the tests.

Peripheral answered 2/3, 2015 at 8:51 Comment(1)
In that case, PowerMock is your best bet. Just remember in the future to always wrap static method calls; they are alluring but make code hard or impossible to test.Peripheral
T
-3

Services can usually be replaced at runtime.

If you are using OSGi you can replace the service implementation in a set up method annotated with @BeforeClass and unregister the mocked implementation in an @AfterClass method:

private ServiceRegistration m_registration;

@BeforeClass
public void setUp() {
  SomeInterface mockedService = Mockito.mock(SomeInterface.class);
  m_registration = registerService(Activator.getDefault().getBundle(), Integer.MAX_VALUE, SomeInterface.class, mockedService);
}

@AfterClass
public void tearDown() {
  if (m_registration != null) {
    unregisterService(m_registration);
  }
}

public static ServiceRegistration registerService(Bundle bundle, int ranking, Class<? extends IService> serviceInterface, Object service) {
  Hashtable<String, Object> initParams = new Hashtable<String, Object>();
  initParams.put(Constants.SERVICE_RANKING, ranking);
  return bundle.getBundleContext().registerService(serviceInterface.getName(), service, initParams);
}

public static void unregisterService(ServiceRegistration registration) {
  registration.unregister();
}
Touter answered 2/3, 2015 at 9:38 Comment(1)
OSGi != ServiceLoader at all.Redbud

© 2022 - 2024 — McMap. All rights reserved.