Java's ServiceLoader and test resources
Asked Answered
U

1

7

I have a web application that defines a Hibernate Integrator as part of the Java ServiceLoader specification like so:

src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator

  # Define integrators that should be instantiated by the ServiceLoader
  org.emmerich.MyIntegrator

This is done according to the Hibernate guide here.

My issue is that when I try to perform unit tests, the main Integrator descriptor is still parsed and instantiated. This means that, because I'm mocking a large portion of the application in unit tests, when the integrator tries to run it encounters errors which causes my tests to fail.

I've defined the same file in the test resources:

src/test/resources/META-INF/services/org.hibernate.integrator.spi.Integrator

  # Empty file to try and overwrite the main deployment description.

but instead I find that both test and main integrator files are parsed.

I expected that the test resource would overwrite the main resource, thus rendering the main resource obscolete, but that's not what happens. As both files are on the classpath (I'm running the tests through Maven with the surefire-plugin, which puts both test-classes and classes on the classpath). A similar thing happens with persistence.xml.

In my unit testing environment I don't want any Integrators to be instantiated because I want to control the construction of these beans as manually as possible. Give that I'm testing units of execution, I don't want additional beans such as Integrators lying around that may affect the running of the tests. I think this is a perfectly legitimate requirement during unit testing. However, whilst the main resources are still parsed by the ServiceLoader, this isn't possible.

The solution I've come to do is based on the persistence.xml solution posted here:

How to configure JPA for testing in Maven

My question is whether there's a better way of excluding main resources from being processed during unit testing than forcing renaming, especially in the context of ServiceLoader files?

To try and summarize it a bit better:

What happens when you have two files both named after the same service interface on the classpath? To me, it seems that all services within both files are instantiated. There is no overwriting, it would seem.

Unimposing answered 19/4, 2013 at 12:38 Comment(0)
U
1

For those interested, a little more investigation lead to the fact that this wasn't really Hibernate's problem, but more the way the ServiceLoader loads in files. From the API:

If a particular concrete provider class is named in more than one configuration file, or is named in the same configuration file more than once, then the duplicates are ignored.

Unfortunately this only pertains to concrete classes. So if I specify two copies of the same file in the form:

org.emmerich.MyServiceInterface

  org.emmerich.MyServiceImpl

then MyServiceImpl is only instantiated once. However, if I specify two files with two different implementation classes then both implementations are instantiated.

This isn't really good for unit testing where you want a bit finer control on where your services are instantiated. I don't really know enough about the design decisions behind it, but Hibernate's use of this makes it harder to unit test.

Anyways, the solution I came up with was to delegate the overriding of main resources to a file I had control over, like a properties file. Inside the service, I now check whether a flag is true or false in that properties file. In my main resource, it's true. In my test, it's false. Because looking up the properties hits the first file on the classpath, I know it'll get the test resource. My structure looks like this:

src
  main
    resources
      META-INF
        services
          org.hibernate.integrator.spi.Integrator # has line MyIntegrator
    application.properties     # shouldIntegrate=true 
  test
    resources
      application.properties   # shouldIntegrate=false

And in MyIntegrator:

public class MyIntegrator implements Integrator {

  @Override
  public void integrate(Configuration configuration, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
    if(shouldIntegrate()) {
      // do integration
    }
  }

  private boolean shouldIntegrate() {
    // read Properties file from classpath
    // getClass().getClassLoader().getResourceAsStream("application.properties")
    // return value of "shouldIntegrate"
  }

When I run in a test environment, the properties file lookup points to the test resource. Outside of a test environment, it points to the main resource.

Unimposing answered 19/4, 2013 at 16:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.