Search code examples
androidandroid-instrumentationinstrumented-testactivity-scenarioandroid-locale

Android instrumented tests - Recreated Activity not picking up locale change, API <33


At least, it's not doing what I'd expect. Here's my situation --

I'm writing a feature to allow that allows users to make locale changes at runtime. Without any special handling, a locale change will automatically recreate Activities and use the updated resources in the refreshed page - namely, the string resources will swap out to a different locale. It's the behavior I currently want.

I'm trying to write an instrumented unit test that simulates and confirms this behavior. Here's what I currently have:

@get:Rule
val activityScenarioRule = activityScenarioRule<MainActivity>()

val appContext = InstrumentationRegistry.getInstrumentation().targetContext

...

 @Test
    fun changeAppLocaleAndGetChangedResource() {
        activityScenarioRule.scenario.use { scenario ->

            // create second standalone configuration to retrieve target string for assertion
            val locale2Config = Configuration(appContext.resources.configuration)
                .apply { setLocale(targetTestLocale) }

            // set target string
            val targetTestString = appContext.createConfigurationContext(locale2Config)
                .resources.getString(R.string.today_i_am)

            scenario.onActivity {
                // initial resource request should pull the default locale string (in English)
                val firstResource = appContext.resources.getString(R.string.my_string)
                assertThat(firstResource).isNotEqualTo(targetTestString)

                // change app locale
                AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(languageTag))
            }

            // recreate activity after locale configuration change
            scenario.recreate()

            scenario.onActivity {
                // second resource request should pull the target locale string (in Spanish
                val newResource = appContext.resources.getString(R.string.my_string)
                assertThat(newResource).isEqualTo(targetTestString)
            }
        }
    }

Here's the catch - this works great running on API 33. It fails for all lower APIs (27-32, in my case).

compileSdk = 33
minSdk = 27
targetSdk = 33

I know that for the locale configuration change to take effect, I need the Activity to restart, so I tried to force that with recreate() (this this seems not needed on API 33). For whatever reason, I'm still pulling the same string resource after the activity is recreated, even though the app locale has changed (confirmed by debug).

Expected string value:
1st time - Hello
2nd time - Hola

Actual string value:
1st time - Hello
2nd time - Hello

What's going on? Am I doing something wrong with the activity scenario? Or maybe I have the wrong idea about what behavior to expect? Any help appreciated. Thanks.


Solution

  • Okay, I've figured this out. Crucially, I left out one small detail in the original post - I was getting my application context through instrumentation, not the activity context.

    val appContext = InstrumentationRegistry.getInstrumentation().targetContext
    

    I've edited the original question to add that for reference (and updated the title a bit).

    I incorrectly believed the targetContext was getting me the context I needed, but what I should have been doing is using the activity itself as the context when fetching the resources.

    Here's the fixed test using...

    • the application context to create the standalone Configuration
    • the activity context to get the string resources before and after locale change
    • no recreate() needed because the process is automatically triggered on config change (as expected)
    @Test
        fun changeAppLocaleAndGetChangedResource() {
            activityScenarioRule.scenario.use { scenario ->
    
                // create second locale to retrieve target string for assertion
                val locale2Config = Configuration(appContext.resources.configuration)
                    .apply { setLocale(targetTestLocale) }
    
                // set target string
                val targetTestString = appContext.createConfigurationContext(locale2Config)
                    .resources.getString(R.string.today_i_am)
    
                scenario.onActivity { activity ->
                    // initial resource request should pull the default locale string
                    val firstResource = activity.resources.getString(R.string.today_i_am)
                    assertThat(firstResource).isNotEqualTo(targetTestString)
    
                    // change app locale
                    localeChanger.changeAppLocale(targetTestLocaleTag)
                }
    
                scenario.onActivity { activity ->
                    // second resource request should pull the target locale string
                    val newResource = activity.resources.getString(R.string.today_i_am)
                    assertThat(newResource).isEqualTo(targetTestString)
                }
            }
        }