Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DroidKaigi2025アプリのスクリーンショットテストが失敗していた原因を調べてみた

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Shun Miyazaki Shun Miyazaki
August 26, 2025
200

 DroidKaigi2025アプリのスクリーンショットテストが失敗していた原因を調べてみた

Avatar for Shun Miyazaki

Shun Miyazaki

August 26, 2025
Tweet

Transcript

  1. 4DSFFOTIPU5FTUͷ࣮૷࣌ʹΤϥʔʹૺ۰ "EE4DSFFO4IPU5FTUGPS$POUSJCVUPST4DSFFO @RunWith(UiTestRunner::class) class ContributorsScreenTest { val testAppGraph = createContributorsScreenTestGraph()

    @ComposeTest fun runTest() { describedBehaviors.forEach { behavior -> val robot = testAppGraph.contributorsScreenRobotProvider() runComposeUiTest { behavior.execute(robot) } } }
  2. 4DSFFOTIPU5FTUͷ࣮૷࣌ʹΤϥʔʹૺ۰ "EE4DSFFO4IPU5FTUGPS$POUSJCVUPST4DSFFO Executing step: 0 (ContributorsScreen - when server is

    operational - it should show loading indicator) Android context is not initialized. If it happens in the Preview mode then call PreviewContextConfigurationEffect() function. java.lang.IllegalStateException: Android context is not initialized. If it happens in the Preview mode then call PreviewContextConfigurationEffect() function.
  3. Τϥʔ͕ൃੜ͍ͯ͠ΔՕॴ @ExperimentalResourceApi internal actual fun getPlatformResourceReader(): ResourceReader = DefaultAndroidResourceReader @ExperimentalResourceApi

    internal object DefaultAndroidResourceReader : ResourceReader { private val assets: AssetManager by lazy { val context = androidContext ?: error( "Android context is not initialized. " + "If it happens in the Preview mode then call PreviewContextConfigurationEffect() function." ) context.assets } private val instrumentedAssets: AssetManager? get() = try { androidInstrumentedContext.assets } catch (e: NoClassDefFoundError) { Log.d("ResourceReader", "Android Instrumentation context is not available.") null } 3FTPVSDF3FBEFSBOESPJE
  4. Τϥʔ͕ൃੜ͍ͯ͠ΔՕॴ @ExperimentalResourceApi internal actual fun getPlatformResourceReader(): ResourceReader = DefaultAndroidResourceReader @ExperimentalResourceApi

    internal object DefaultAndroidResourceReader : ResourceReader { private val assets: AssetManager by lazy { val context = androidContext ?: error( "Android context is not initialized. " + "If it happens in the Preview mode then call PreviewContextConfigurationEffect() function." ) context.assets } private val instrumentedAssets: AssetManager? get() = try { androidInstrumentedContext.assets } catch (e: NoClassDefFoundError) { Log.d("ResourceReader", "Android Instrumentation context is not available.") null } 3FTPVSDF3FBEFSBOESPJE
  5. Τϥʔ͕ൃੜ͍ͯ͠ΔՕॴ internal val androidContext get() = AndroidContextProvider.ANDROID_CONTEXT internal class AndroidContextProvider

    : ContentProvider() { companion object { @SuppressLint("StaticFieldLeak") var ANDROID_CONTEXT: Context? = null } override fun onCreate(): Boolean { ANDROID_CONTEXT = context return true } "OESPJE$POUFYU1SPWJEFS
  6. <༨ஊ>$POUFYUΛઃఆ͍ͯ͠Δίʔυ try { final java.lang.ClassLoader cl = c.getClassLoader(); LoadedApk packageInfo

    = peekPackageInfo(ai.packageName, true); if (packageInfo == null) { // System startup case. packageInfo = getSystemContext().mPackageInfo; } localProvider = packageInfo.getAppFactory() .instantiateProvider(cl, info.name); provider = localProvider.getIContentProvider(); if (provider == null) { Slog.e(TAG, "Failed to instantiate class " + info.name + " from sourceDir " + info.applicationInfo.sourceDir); return null; } if (DEBUG_PROVIDER) Slog.v( TAG, "Instantiating local provider " + info.name); // XXX Need to create the correct context for this provider. localProvider.attachInfo(c, info); } "DUJWJUZ5ISFBE
  7. <௒௒௒༨ஊ>%FGBVMU*0T3FTPVSDF3FBEFS @ExperimentalResourceApi internal object DefaultIOsResourceReader : ResourceReader { private val

    composeResourcesDir: String by lazy { findComposeResourcesPath() } override suspend fun read(path: String): ByteArray { val data = readData(getPathInBundle(path)) return ByteArray(data.length.toInt()).apply { usePinned { memcpy(it.addressOf(0), data.bytes, data.length) } } } override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray { val data = readData(getPathInBundle(path), offset, size) return ByteArray(data.length.toInt()).apply { usePinned { memcpy(it.addressOf(0), data.bytes, data.length) } } } ࣮૷͕͓΋Ζ͍
  8. 1SFWJFX$POUFYU$PO fi HVSBUJPO& ff FDU /** * The function configures

    the android context * to be used for non-composable resource read functions * * e.g. `Res.readBytes(...)` * * Example usage: * ``` * @Preview * @Composable * fun MyPreviewComponent() { * PreviewContextConfigurationEffect() * //... * } * ``` */ @Composable fun PreviewContextConfigurationEffect() { if (LocalInspectionMode.current) { AndroidContextProvider.ANDROID_CONTEXT = LocalContext.current } } 1SFWJFXͷͱ͖͸"OESPJE$POUFYU1SPWJEFSͷ $POUFYU͕ઃఆ͞Ε͍ͯͳ͍ͷͰɺ 1SFWJFX$POUFYU$PO fi HVSBUJPO& ff FDUΛ ࢖ͬͯͶͱ͍͏͜ͱΒ͍͠
  9. "OESPJE5FTU"QQ(SBQIͷطଘ࣮૷ internal interface AndroidTestAppGraph : TestAppGraph { @SingleIn(AppScope::class) @Provides fun

    provideContext(): Context { val testContext = ApplicationProvider.getApplicationContext<Context>() // Workaround for "Android context is not initialized" // FYI: https://youtrack.jetbrains.com/issue/CMP-6676/Android-context-is-not- initialized-when-removing-AndroidContextProvider val providerClass = Class.forName("org.jetbrains.compose.resources.AndroidContextProvider") val provider = providerClass.getDeclaredConstructor().newInstance() providerClass.getMethod("access\$setANDROID_CONTEXT\$cp", Context::class.java) .invoke(provider, testContext) return testContext } } ϦϨΫγϣϯΛ࢖༻ͯ͠ "OESPJE$POUFYU1SPWJEFSͷ$POUFYUΛηοτ
  10. "OESPJE5FTU"QQ(SBQIͷमਖ਼ internal interface AndroidTestAppGraph : TestAppGraph { val context: Context

    @SingleIn(AppScope::class) @Provides fun provideContext(): Context { val testContext = ApplicationProvider.getApplicationContext<Context>() // Workaround for "Android context is not initialized" // FYI: https://youtrack.jetbrains.com/issue/CMP-6676/Android-context-is-not-initialized-when-removing- AndroidContextProvider val providerClass = Class.forName("org.jetbrains.compose.resources.AndroidContextProvider") val provider = providerClass.getDeclaredConstructor().newInstance() providerClass.getMethod("access\$setANDROID_CONTEXT\$cp", Context::class.java) .invoke(provider, testContext) return testContext } } internal actual fun createTestAppGraph(): TestAppGraph { return createGraph<AndroidTestAppGraph>().also { // Ensure context is initialized before Compose UI tests it.context } }
  11. "OESPJE5FTU"QQ(SBQIͷमਖ਼ internal interface AndroidTestAppGraph : TestAppGraph { val context: Context

    @SingleIn(AppScope::class) @Provides fun provideContext(): Context { val testContext = ApplicationProvider.getApplicationContext<Context>() // Workaround for "Android context is not initialized" // FYI: https://youtrack.jetbrains.com/issue/CMP-6676/Android-context-is-not-initialized-when-removing- AndroidContextProvider val providerClass = Class.forName("org.jetbrains.compose.resources.AndroidContextProvider") val provider = providerClass.getDeclaredConstructor().newInstance() providerClass.getMethod("access\$setANDROID_CONTEXT\$cp", Context::class.java) .invoke(provider, testContext) return testContext } } internal actual fun createTestAppGraph(): TestAppGraph { return createGraph<AndroidTestAppGraph>().also { // Ensure context is initialized before Compose UI tests it.context } } ࣮֬ʹQSPWJEF$POUFYU͕૸ΔΑ͏ʹ
  12. 5JNFUBCMF4DSFFO5FTU @RunWith(UiTestRunner::class) class TimetableScreenTest { val testAppGraph = createTimetableScreenTestGraph() @ComposeTest

    fun runTest() { describedBehaviors.forEach { behavior -> val robot = testAppGraph.timetableScreenRobotProvider() runComposeUiTest { behavior.execute(robot) } } }
  13. ͦͯ͠ḷΓண͍ͨ"OESPJE%BUB(SBQI @ContributesTo(DataScope::class) public interface AndroidDataGraph { @Provides public fun provideDataStorePathProducer(context:

    Context): DataStorePathProducer { return DataStorePathProducer { fileName -> context.cacheDir.resolve(fileName).path } } @Provides public fun provideHttpClient(json: Json): HttpClient { return HttpClient(OkHttp) { defaultKtorConfig(json) } } }
  14. ͦͯ͠ḷΓண͍ͨ"OESPJE%BUB(SBQI @ContributesTo(DataScope::class) public interface AndroidDataGraph { @Provides public fun provideDataStorePathProducer(context:

    Context): DataStorePathProducer { return DataStorePathProducer { fileName -> context.cacheDir.resolve(fileName).path } } @Provides public fun provideHttpClient(json: Json): HttpClient { return HttpClient(OkHttp) { defaultKtorConfig(json) } } }
  15. मਖ਼Ҋ internal interface AndroidTestAppGraph : TestAppGraph { val context: Context

    @SingleIn(AppScope::class) @Provides fun provideContext(): Context { val testContext = ApplicationProvider.getApplicationContext<Context>() // Workaround for "Android context is not initialized" // FYI: https://youtrack.jetbrains.com/issue/CMP-6676/Android-context-is-not-initialized- when-removing-AndroidContextProvider val providerClass = Class.forName("org.jetbrains.compose.resources.AndroidContextProvider") val provider = providerClass.getDeclaredConstructor().newInstance() providerClass.getMethod("access\$setANDROID_CONTEXT\$cp", Context::class.java) .invoke(provider, testContext) return testContext } } ॳظઃఆͱґଘؔ܎ͷఏڙͱ͍͏ ̎ͭͷ੹຿Λ୲͍ͬͯΔ
  16. मਖ਼Ҋ internal interface AndroidTestAppGraph : TestAppGraph { @SingleIn(AppScope::class) @Provides fun

    provideContext(): Context { return ApplicationProvider.getApplicationContext() } } private fun initializeAndroidContext() { val testContext = ApplicationProvider.getApplicationContext<Context>() val providerClass = Class.forName("org.jetbrains.compose.resources.AndroidContextProvider") val provider = providerClass.getDeclaredConstructor().newInstance() providerClass.getMethod("access\$setANDROID_CONTEXT\$cp", Context::class.java) .invoke(provider, testContext) } internal actual fun createTestAppGraph(): TestAppGraph { initializeAndroidContext() return createGraph<AndroidTestAppGraph>() } ॲཧΛ෼͚Δ