【问题标题】:In Java, how can I mock a service loaded using ServiceLoader?在 Java 中,如何模拟使用 ServiceLoader 加载的服务?
【发布时间】:2015-03-02 08:49:03
【问题描述】:

我有一个旧版 Java 应用程序,其中包含类似这样的代码

ServiceLoader.load(SomeInterface.class)

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

很遗憾,我无法更改旧代码,并且我不希望静态添加任何内容(例如,向 META-INF 添加内容)。

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

【问题讨论】:

  • 您是否可以仅出于测试目的更改应用程序代码?

标签: java mockito serviceloader


【解决方案1】:

您可以使用 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));
    }
}

【讨论】:

  • 这可能是最好的答案,尽管我在过去操纵代码的模拟框架方面有过糟糕的经历(例如,jMockit 会导致非常奇怪的行为)。我可能会用间接调用这个遗留代码的几十个地方包装,这将允许我从测试中插入遗留代码的模拟版本。
  • @GraemeMoss:只是另一个想法,如果在构造时调用 ServiceLoader 方法,并且您可以“介于”包含类的构造和加载的服务类的实际使用之间,您在实际使用之前,可能会使用反射将持有对已加载服务类的引用的成员变量替换为对您的模拟的引用。
  • 我曾想过这一点,但该领域是最终的。 :-(
【解决方案2】:

来自ServiceLoader.load 文档:

为给定的服务类型创建一个新的服务加载器,使用 当前线程的上下文类加载器。

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

如果用于提供程序加载的类加载器的类路径 包括远程网络 URL,然后这些 URL 将被取消引用 搜索提供程序配置文件的过程。

上下文类加载器还需要加载服务类的模拟实现,然后作为模拟实现传递。

这样的上下文类加载器需要做两件事:

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

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

【讨论】:

  • 这听起来很沉重。我将如何创建这样的类加载器?
  • 有趣的方法,但在实践中不幸地失败了。我设法让第一部分工作,但最终它与将提供者配置放在 Maven 的 src/test/resources 目录中的效果相同。不值得努力。第二部分只能通过繁重的字节码操作才能实现,因为它首先加载类,然后实例化它。但更重要的是:这个解决方案是建立在对 ServiceLoader 和 ClassLoader 类的实现细节的假设之上的,不能保证在 Java 版本之间保持稳定。
【解决方案3】:

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

【讨论】:

  • 在这种情况下,PowerMock 是您最好的选择。只要记住以后总是包装静态方法调用;它们很诱人,但会使代码难以或无法测试。
【解决方案4】:

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

如果您使用 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();
}

【讨论】:

  • OSGi != ServiceLoader。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-03-10
  • 1970-01-01
  • 1970-01-01
  • 2018-09-13
  • 1970-01-01
  • 2023-04-11
  • 1970-01-01
相关资源
最近更新 更多