在 Java 中,如何模拟使用 ServiceLoader 加载的服务?

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

我有一个遗留 Java 应用程序,它的代码类似于这样

ServiceLoader.load(SomeInterface.class)

并且我想提供 SomeInterface 的模拟实现供此代码使用。我使用 mockito 模拟框架。

不幸的是,我无法更改遗留代码,而且我不想静态添加任何内容(例如,向 META-INF 添加内容)。

有没有一种简单的方法可以从测试中做到这一点,即。在测试运行时?

将调用移动到受保护的方法中并在测试中覆盖它。这允许您在测试期间 return 任何事情。

来自 ServiceLoader.load 文档:

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

因此您可以在测试运行期间使用特殊的上下文 class 加载程序,它将在 META-INF/service 中动态生成提供程序配置文件。由于 ServiceLoader 文档中的注释,上下文 class 加载程序将用于搜索提供程序配置文件:

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.

上下文 class 加载程序还需要加载服务 class 的模拟实现,然后将其作为模拟实现传递。

这样的上下文 class 加载程序需要做两件事:

  • 根据请求动态生成提供程序配置文件 根据 getResource* 方法
  • 动态生成一个class(例如 根据 loadClass 方法的要求使用 ASM library),如果它是 class 在动态生成的提供程序中指定的 配置文件

使用上述方法,您无需更改现有代码。

您可以使用 PowerMockito 和 Mockito 一起模拟静态方法:

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

服务通常可以在运行时替换。

如果您使用的是 OSGi,您可以在用 @BeforeClass 注释的设置方法中替换服务实现,并在 @AfterClass 中注销模拟实现方法:

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