Prabhath
Prabhath

Reputation: 603

Unit test ServiceLoader

I have a method that uses ServiceLoader to load services using resources.

public List<String> getContextData(int Id)
{
  List<String> list = new ArrayList<String>();
  ServiceLoader<ContextPlugin> serviceLoader =  ServiceLoader.load(ContextPlugin.class);
  for (Iterator<ContextPlugin> iterator = serviceLoader.iterator(); iterator.hasNext();)
  {
    list .addAll(iterator.next().getContextData(Id));
  }
  return list;
}

How should I unit test above method using Junit?

Upvotes: 15

Views: 5860

Answers (2)

Julien Kronegg
Julien Kronegg

Reputation: 5251

One test configuration

If you have only one configuration to test, you can use a test/resources/META-INF/services/mypackage.MyInterface file, see @SubOptimal answer. However, if you need to test more than one configuration, this approach does not work.

Multiple test configurations

To test more than configuration, I'm using a custom classloader which overrides the META-INF/services/mypackage.MyInterface. Basically, you use this classloader as parameter in ServiceLoader.load(MyInterface.class, customClassLoader).

The advantage of this approach is that it does not rely on mocking libraries such as Mockito or Powermock.

To create a classloader without META-INF/services/mypackage.MyInterface file:

new ServiceLoaderTestClassLoader(MyInterface.class)

To create a classloader with a META-INF/services/mypackage.MyInterface file containing two lines with classes SubClassOfMyInterface and OtherSubClassOfMyInterface:

new ServiceLoaderTestClassLoader(MyInterface.class, SubClassOfMyInterface.class, OtherSubClassOfMyInterface.class) 

This is the custom classloader source code:

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.Collections;
import java.util.Enumeration;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Testing classloader for ServiceLoader.
 * This classloader overrides the META-INF/services/<interface> file with a custom definition.
 */
public class ServiceLoaderTestClassLoader extends URLClassLoader {
    Class<?> metaInfInterface;
    Class<?>[] implementingClasses;

    /**
     * Constructs a classloader which has no META-INF/services/<metaInfInterface>.
     *
     * @param metaInfInterface ServiceLoader interface
     */
    public ServiceLoaderTestClassLoader(Class<?> metaInfInterface) {
        this(metaInfInterface, (Class<?>[]) null);
    }

    /**
     * Constructs a fake META-INF/services/<metaInfInterface> file which contains the provided array of classes.
     * When the implementingClasses array is null, the META-INF file will not be constructed.
     * The classes from implementingClasses are not required to implement the metaInfInterface.
     *
     * @param metaInfInterface    ServiceLoader interface
     * @param implementingClasses potential subclasses of the ServiceLoader metaInfInterface
     */
    public ServiceLoaderTestClassLoader(Class<?> metaInfInterface, Class<?>... implementingClasses) {
        super(new URL[0], metaInfInterface.getClassLoader());
        if (!metaInfInterface.isInterface()) {
            throw new IllegalArgumentException("the META-INF service " + metaInfInterface + " should be an interface");
        }
        this.metaInfInterface = metaInfInterface;
        this.implementingClasses = implementingClasses;
    }

    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        if (name.equals("META-INF/services/" + metaInfInterface.getName())) {
            if (implementingClasses == null) {
                return Collections.emptyEnumeration();
            }
            URL url = new URL("foo", "bar", 99, "/foobar", new URLStreamHandler() {
                @Override
                protected URLConnection openConnection(URL u) {
                return new URLConnection(u) {
                    @Override
                    public void connect() {
                    }

                    @Override
                    public InputStream getInputStream() throws IOException {
                        return new ByteArrayInputStream(Stream.of(implementingClasses)
                                .map(Class::getName)
                                .collect(Collectors.joining("\n"))
                                .getBytes());
                    }
                };
                }
            });

            return new Enumeration<>() {
                boolean hasNext = true;

                @Override
                public boolean hasMoreElements() {
                    return hasNext;
                }

                @Override
                public URL nextElement() {
                    hasNext = false;
                    return url;
                }
            };
        }
        return super.getResources(name);
    }

}

Upvotes: 2

SubOptimal
SubOptimal

Reputation: 22973

You need to copy the "provider-configuration file" into your test class directory.

assuming your test class files are located at

test/classes/

you need to copy the "provider-configuration file" to

test/classes/META-INF/services/your.package.ContextPlugin

How to copy the files depend on your build tool (e.g. maven, gradle, ant, ...)

As example for maven you should store them in the test resources folder.

src/test/resources/META-INF/services/your.package.ContextPlugin

Upvotes: 8

Related Questions