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

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

Avatar for Shun Miyazaki Shun Miyazaki
August 26, 2025
160

 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>() } ॲཧΛ෼͚Δ