In Java, how can I mock a service loaded using Ser

2020-08-10 08:42发布

问题:

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?

回答1:

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));
    }
}


回答2:

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.



回答3:

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



回答4:

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();
}