Skip to content

The Surefire locale trap: why -Duser.language doesn't work

If you have ever written a test that passes on your English development machine but fails on a CI server with a different OS locale — or vice versa — you have probably encountered this trap.


You have a test that constructs a localizable object and then asserts an English string value from its resource bundle:

@Test
void testInitialProperties() {
MyFrame frame = new MyFrame();
assertEquals("File", frame.getResourceBundle().getString("fileMenu"));
}

The test passes on your machine. On a CI runner configured with LANG=fr_FR.UTF-8, it fails with:

expected: <File> but was: <Fichier>

You add -Duser.language=en to your Maven Surefire configuration. The test still fails.


The JVM resolves Locale.getDefault() once, at startup, from OS locale settings. It stores the result internally before any application code runs.

Maven Surefire forks a new JVM to run tests. That forked JVM has already initialized its default locale from the OS by the time Surefire injects -D system properties via System.setProperty(). At that point Locale.getDefault() has already been called and cached by the JVM internals.

System.setProperty("user.language", "en") changes the system property, but Locale.getDefault() does not re-read user.language after startup. It returns whatever locale the JVM initialized at launch.

The result: -Duser.language=fr on the Maven command line or in <argLine> has no effect on Locale.getDefault() in the forked test JVM.


Why this bites LocalizationDelegate specifically

Section titled “Why this bites LocalizationDelegate specifically”

LocalizationDelegate initializes its locale field from Locale.getDefault() at the point of object construction:

private volatile Locale locale = Locale.getDefault();

This is the correct behavior: a freshly constructed Localizable should start with the application’s default locale. But it means that if Locale.getDefault() returns a non-English locale at test time, any Localizable object constructed before an explicit setBundleLocale() call will load its bundle in that non-English locale. An assertion against an English string will then fail.


In test-only Localizable factory methods, call setBundleLocale(Locale.ROOT) before any locale-dependent work:

public static TestComponentSource create() {
TestComponentSource source = new TestComponentSource();
source.setBundleLocale(Locale.ROOT); // pin before updateLocaleSpecificValues()
return source;
}

Calling setBundleLocale() overrides whatever Locale.getDefault() returned at construction time. Because it fires before updateLocaleSpecificValues(), every subsequent locale-dependent operation sees Locale.ROOT.

When calling ResourceBundle.getBundle() directly in tests, always pass an explicit locale:

// ✗ uses Locale.getDefault() — locale-dependent, may fail on non-English systems
ResourceBundle rb = ResourceBundle.getBundle("com.example.MyFrameBundle");
// ✓ explicit locale — deterministic regardless of OS settings
ResourceBundle rb = ResourceBundle.getBundle("com.example.MyFrameBundle", Locale.ROOT);

In any test that asserts specific string values from a resource bundle, always pin the locale to an explicit value — Locale.ROOT, Locale.ENGLISH, or whichever locale the strings are in — before constructing the Localizable object or calling getBundle().

Never rely on Locale.getDefault() in tests that assert bundle content. It is a JVM-initialization artifact, not a controlled test input.


What about testing Locale.getDefault() behavior?

Section titled “What about testing Locale.getDefault() behavior?”

Some tests legitimately test the contract that a freshly constructed Localizable starts with Locale.getDefault(). Those tests are intentionally locale-dependent and should not pin the locale — that would defeat the purpose.

The rule applies only to tests that assert specific string values. Tests that assert which locale was used (not what string was returned) are fine without pinning.


To confirm your tests are locale-robust, run them under an explicit non-English locale using the JVM launch argument form:

<!-- pom.xml Surefire configuration -->
<configuration>
<argLine>-Duser.language=fr -Duser.country=FR</argLine>
</configuration>

When passed in <argLine> as literals (not as property references), these flags are part of the java command that starts the fork and are processed before locale initialization. If your tests pass under both en and fr, they are locale-robust.