When running a test program in an unnamed module under JUnit the ResourceBundleControlProvider.getControl()
method is not invoked.
I have this implementation of ResourceBundleControlProvider
:
import java.util.ResourceBundle;
import java.util.spi.ResourceBundleControlProvider;
/**
* A ResourceBundleControlProvider implementation
*/
public class MyControlProvider implements ResourceBundleControlProvider {
public ResourceBundle.Control getControl(String baseName) {
System.out.println("In getControl, baseName: " + baseName);
return null;
}
}
I have the following in META-INF/services/java.util.spi.ResourceBundleControlProvider
:
MyControlProvider
:
This test program is intended to demonstrate that MyControlProvider.getControl(String)
is invoked:
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.ServiceLoader;
import java.util.spi.ResourceBundleControlProvider;
import org.junit.jupiter.api.Test;
class TestProvider
{
/**
* Attempt to load a missing resource bundle - should throw
* MissingResourceException and write a message to stdout.
*/
@Test
public void testProvider() {
try {
ResourceBundle.getBundle("foo");
}
catch(MissingResourceException e) {
System.out.println("Caught: " + e.getMessage());
}
ServiceLoader<ResourceBundleControlProvider> loader =
ServiceLoader.loadInstalled(ResourceBundleControlProvider.class);
for (ResourceBundleControlProvider provider : loader) {
System.out.println("Found installed provider instance of: " + provider.getClass().getName());
}
ServiceLoader<ResourceBundleControlProvider> loader2 = ServiceLoader.load(ResourceBundleControlProvider.class);
for (ResourceBundleControlProvider provider : loader2) {
System.out.println("Found provider instance of: " + provider.getClass().getName());
}
Module mod = this.getClass().getModule();
if (mod.isNamed()) {
System.out.println("Module: " + mod.getName());
}
else {
System.out.println("Unnamed Module: " + mod.toString());
}
}
public static void main(String[] args) {
TestProvider tp = new TestProvider();
tp.testProvider();
}
}
When I run the test program directly, I see that the getControl() method is invoked:
java -cp . TestProvider
In getControl, baseName: foo
Caught: Can't find bundle for base name foo, locale en_US
Found provider instance of: MyControlProvider
Unnamed Module: unnamed module @5caf905d
When I run the test under JUnit it is not:
java -jar ../junit/junit-platform-console-standalone-1.13.0-M2.jar execute -cp . --scan-classpath
💚 Thanks for using JUnit! Support its development at https://junit.org/sponsoring
Caught: Can't find bundle for base name foo, locale en_US
Found provider instance of: MyControlProvider
Unnamed Module: unnamed module @565f390
â•·
├─ JUnit Platform Suite ✔
├─ JUnit Jupiter ✔
│ └─ TestProvider ✔
│ └─ testProvider() ✔
└─ JUnit Vintage ✔
Test run finished after 188 ms
The JDK docs for ResourceBundleControlProvider
state that "All ResourceBundleControlProviders
are ignored in named modules.". The JUnit test run appears to be running from an unnamed module, so it seems that in this case it should not be ignored.
I am running this version of JUnit, JDK and MacOS:
JUnit Platform Console Launcher 1.13.0-M2
JVM: 21.0.6 (Eclipse Adoptium OpenJDK 64-Bit Server VM 21.0.6+7-LTS)
OS: Mac OS X 15.4.1 aarch64
The problem has to do with how JUnit 5 is loading your code. When you execute this command:
java -jar ../junit/junit-platform-console-standalone-1.13.0-M2.jar execute -cp . --scan-classpath
Then JUnit 5 has to use its own ClassLoader
to load the classes and other resources you specified
via -cp
. Note that -cp
is an argument to the JUnit console launcher, not to Java itself. It's
an option used to specify additional class-path entries:
-cp, --classpath, --class-path=PATH Provide additional classpath entries -- for example, for adding engines and their dependencies. This option can be repeated.
The entries on this path will not be visible to the system class loader. They will only be visible to JUnit's own class loader.
This is a problem because ResourceBundle
apparently hard-codes the use of the
system class loader to find providers of ResourceBundleControlProvider
. I'm not sure if this is
documented somewhere, but you can see this in the source code (link for Java 24):
private static class ResourceBundleControlProviderHolder { private static final List<ResourceBundleControlProvider> CONTROL_PROVIDERS = ServiceLoader.load(ResourceBundleControlProvider.class, ClassLoader.getSystemClassLoader()).stream() .map(ServiceLoader.Provider::get) .toList(); private static Control getControl(String baseName) { return CONTROL_PROVIDERS.isEmpty() ? Control.INSTANCE : CONTROL_PROVIDERS.stream() .flatMap(provider -> Stream.ofNullable(provider.getControl(baseName))) .findFirst() .orElse(Control.INSTANCE); } }
Your ResourceBundleControlProvider
implementation and service configuration file are part of your
test code. That code is being loaded by JUnit's class loader, not the system class loader. Thus, your
provider is not found at runtime.
The solution is to make sure your code, or at least your ResourceBundleControlProvider
implementation (and service configuration file), is loaded by the system class loader. One way to
do this is with the following command:
java -cp <path> org.junit.platform.console.ConsoleLauncher --scan-classpath
Note the different location of the -cp
option. Now it's a JVM option instead of an argument of
the JUnit console launcher application. In other words, the entries of <path>
—which includes
your code and the JUnit launcher—will be loaded by the system class loader.