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

在Java中,如何模拟使用ServiceLoader加载的服务?,java,mockito,serviceloader,Java,Mockito,Serviceloader,我有一个遗留Java应用程序,其代码如下 ServiceLoader.load(SomeInterface.class) 我想为这段代码提供SomeInterface的模拟实现。我使用mockito mocking框架 不幸的是,我无法更改遗留代码,我不希望静态地添加任何内容(例如,向META-INF添加内容) 是否有一种简单的方法可以在测试中执行此操作,即在测试运行时执行此操作?将调用移动到受保护的方法中,并在测试中重写它。这允许您在测试期间返回任何内容。来自文档: 使用 当前线程的上下文类

我有一个遗留Java应用程序,其代码如下

ServiceLoader.load(SomeInterface.class)
我想为这段代码提供SomeInterface的模拟实现。我使用mockito mocking框架

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


是否有一种简单的方法可以在测试中执行此操作,即在测试运行时执行此操作?

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

来自文档:

使用 当前线程的上下文类加载器

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

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

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

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

  • 根据请求动态生成提供程序配置文件 每
    getResource*
    方法
  • 动态生成一个类(例如 根据
    loadClass
    方法(如果是 在动态生成的提供程序中指定的类 配置文件
使用上述方法,您不需要更改现有代码。

您可以与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();
}
私人服务注册m_注册;
@课前
公共作废设置(){
SomeInterface mockedService=Mockito.mock(SomeInterface.class);
m_registration=registerService(Activator.getDefault().getBundle(),Integer.MAX_值,SomeInterface.class,mockedService);
}
@下课
公共无效拆卸(){
如果(m_注册!=null){
取消注册服务(m_注册);
}
}

公共静态服务注册注册表服务(Bundle Bundle,int ranking,classing你能仅仅为了测试目的而更改应用程序代码吗?这听起来很沉重。我该如何创建这样一个类加载器?很有趣的方法,但在实践中不幸失败了。我成功地使第一部分工作,但最终它的效果与将提供者配置为在Maven的
src/test/resources
目录中进行初始化。不值得这样做。第二部分只可能使用大量字节码操作,因为它首先加载类,然后对其进行实例化。但更重要的是:此解决方案基于对
ServiceLoader
ClassLoa的实现细节的假设der
类,这些类不能保证在Java版本中保持稳定。在这种情况下,
PowerMock
是您最好的选择。请记住,在将来总是包装静态方法调用;它们很有吸引力,但会使代码很难或根本无法测试。OSGi!=ServiceLoader。这可能是最好的答案,尽管我有过不好的经验过去操纵代码的模拟框架(例如,jMockit会导致非常奇怪的行为)。我可能会用一个间接方法来包装调用此遗留代码的许多地方,该间接方法将允许我从测试中插入遗留代码的模拟版本。@Graemoss:另一个想法是,如果在构造时调用ServiceLoader方法,您可以得到“中间”包含类的构造&加载的服务类的实际用法,在实际使用之前,您可能可以使用反射将包含加载的服务类引用的成员变量替换为对mock的引用。我已经想到了这一点,但是字段是最终的:-(