Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Espresso generating FileNotFoundException, when used with Dagger

I have been wrestling with a legacy Android app, attempting to add testing and proper architecture to it. The app has a main LaunchActivity which runs a series of checks on launch. Originally, the activity was using Dagger to, rather poorly, 'inject dependencies' that the activity would use to run checks.

I shifted gears to MVVM, so that I could test the view model separately, without instrumentation, and would only need to inject a mocked view model for UI tests. I followed this article to introduce the changes, including switching to using the new Dagger Android methods like AndroidInjection.inject.

I want the tests to guide any changes as much as I can, so when I had the basic architecture working, I switched to writing UI tests. Now, having to inject a mock view model into the activity with Dagger was proving to be quite a task, but I think I have reached a workable solution.

I was already using a TestApp with a custom instrumentation runner to use DexOpener, which I changed to also implement HasActivityInjector, much like the actual custom App for my application (both extend Application).

For Dagger, I created separate modules and a component for testing:

TestAppComponent

@Component(
        modules = [
            TestDepsModule::class,
            TestViewModelModule::class,
            TestAndroidContributorModule::class,
            AndroidSupportInjectionModule::class
        ]
)
@Singleton
interface TestAppComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: Application): Builder

        fun testViewModelModule(testViewModelModule: TestViewModelModule): Builder

        fun build(): TestAppComponent
    }

    fun inject(app: TestFieldIApp)
}

TestViewModelModule

@Module
class TestViewModelModule {
    lateinit var mockLaunchViewModel: LaunchViewModel

    @Provides
    fun bindViewModelFactory(factory: TestViewModelFactory): ViewModelProvider.Factory {
        return factory
    }

    @Provides
    @IntoMap
    @ViewModelKey(LaunchViewModel::class)
    fun launchViewModel(): ViewModel {
        if(!(::mockLaunchViewModel.isInitialized)) {
            mockLaunchViewModel = mock(LaunchViewModel::class.java)
        }
        return mockLaunchViewModel
    }
}

TestAndroidConributorModule

@Module
abstract class TestAndroidContributorModule {
    @ContributesAndroidInjector
    abstract fun contributeLaunchActivity(): LaunchActivity
}

Then, in the LaunchActivityTest, I have:

@RunWith(AndroidJUnit4::class)
class LaunchActivityTest {
    @Rule
    @JvmField
    val activityRule: ActivityTestRule<LaunchActivity> = ActivityTestRule(LaunchActivity::class.java, true, false)

    lateinit var viewModel: LaunchViewModel

    @Before
    fun init() {
        viewModel = mock(LaunchViewModel::class.java)

        val testApp: TestLegacyApp = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestLegacyApp

        val testViewModelModule: TestViewModelModule = TestViewModelModule()
        testViewModelModule.mockLaunchViewModel = viewModel

        DaggerTestAppComponent
                .builder()
                .application(testApp)
                .testViewModelModule(testViewModelModule)
                .build()
                .inject(testApp)
    }

    @Test
    fun whenHideInstructionsIsFalse_showsInstructions() {
        `when`(viewModel.hideInstructions).thenReturn(false)

        activityRule.launchActivity(null)

        onView(withId(R.id.launch_page_slider)).check(matches(isDisplayed()))
        onView(withId(R.id.launch_progress_view)).check(matches(not(isDisplayed())))
    }

    @Test
    fun whenHideInstructionsIsTrue_doesNotShowInstructions() {
        `when`(viewModel.hideInstructions).thenReturn(true)

        activityRule.launchActivity(null)

        onView(withId(R.id.launch_page_slider)).check(matches(not(isDisplayed())))
        onView(withId(R.id.launch_progress_view)).check(matches(isDisplayed()))
    }
}

The result is that the view model is being properly mocked, so everything else should work... But, when the Espresso tests are run, although the tests show that they have passed, there is a strange stack trace where the (passing) view assertions ought to be.

E/System: Unable to open zip file: /data/user/0/com.myapps.android.legacyapp/cache/qZb3CT3H.jar
E/System: java.io.FileNotFoundException: File doesn't exist: /data/user/0/com.myapps.android.legacyapp/cache/qZb3CT3H.jar
        at java.util.zip.ZipFile.<init>(ZipFile.java:215)
        at java.util.zip.ZipFile.<init>(ZipFile.java:152)
        at java.util.jar.JarFile.<init>(JarFile.java:160)
        at java.util.jar.JarFile.<init>(JarFile.java:97)
        at libcore.io.ClassPathURLStreamHandler.<init>(ClassPathURLStreamHandler.java:47)
        at dalvik.system.DexPathList$Element.maybeInit(DexPathList.java:702)
        at dalvik.system.DexPathList$Element.findResource(DexPathList.java:729)
        at dalvik.system.DexPathList.findResources(DexPathList.java:526)
        at dalvik.system.BaseDexClassLoader.findResources(BaseDexClassLoader.java:174)
        at java.lang.ClassLoader.getResources(ClassLoader.java:839)
        at java.util.ServiceLoader$LazyIterator.hasNextService(ServiceLoader.java:349)
        at java.util.ServiceLoader$LazyIterator.hasNext(ServiceLoader.java:402)
        at java.util.ServiceLoader$1.hasNext(ServiceLoader.java:488)
        at androidx.test.internal.platform.ServiceLoaderWrapper.loadService(ServiceLoaderWrapper.java:46)
        at androidx.test.espresso.base.UiControllerModule.provideUiController(UiControllerModule.java:42)
        at androidx.test.espresso.base.UiControllerModule_ProvideUiControllerFactory.provideUiController(UiControllerModule_ProvideUiControllerFactory.java:36)
        at androidx.test.espresso.base.UiControllerModule_ProvideUiControllerFactory.get(UiControllerModule_ProvideUiControllerFactory.java:26)
        at androidx.test.espresso.base.UiControllerModule_ProvideUiControllerFactory.get(UiControllerModule_ProvideUiControllerFactory.java:9)
        at androidx.test.espresso.core.internal.deps.dagger.internal.DoubleCheck.get(DoubleCheck.java:51)
        at androidx.test.espresso.DaggerBaseLayerComponent$ViewInteractionComponentImpl.viewInteraction(DaggerBaseLayerComponent.java:239)
        at androidx.test.espresso.Espresso.onView(Espresso.java:84)
        at com.myapps.android.legacyapp.tests.ui.launch.LaunchActivityTest.whenHideInstructionsIsFalse_showsInstructions(LaunchActivityTest.kt:64)

The statement in LaunchActivityTest where the error traces to is:

onView(withId(R.id.launch_page_slider)).check(matches(isDisplayed()))

I can't figure out why the test is showing this error. I know it's something related to Dagger, because if I comment out building DaggerTestAppComponent, there is no issue. But, without using this test component, I'm not sure how I can inject the mocked view model into the activity. Something is causing Dagger and Espresso to not play nicely, something, I think, related to this DaggerBaseLayerComponent in the stack trace. But I have nothing else.

The only 'solution' I have presently is switching to a Fragment instead of an Activity, where I could skip the need for Dagger in tests altogether and follow this sample, but I'm really baffled as to why I'm getting this issue. I would greatly appreciate any help in finding out the reason.

like image 449
user1439707 Avatar asked Sep 23 '19 11:09

user1439707


2 Answers

This is neither based upon a non-stock ROM nor Espresso or Dagger, but it's a known issue,
which appears to stem from the androidx.test.runner.AndroidJUnitRunner in combintion with ActivityScenario or FragmentScenario (as the stack-traces posted there may suggest).

Not using an ActivityTestRule might currently be the only option to work around this. Just star the issue on the issue-tracker and get notified, when something moves forward concerning the issue.

like image 175
Martin Zeitler Avatar answered Nov 16 '22 15:11

Martin Zeitler


Weirdly enough I sometimes managed to resolve this by just uninstalling all app and test related stuff on my device OR by just using a new emulator image and then re-running/installing everything in a clean new state. I'm guessing that it had something to do with my dynamic feature module setup and Android Studio not properly (re)installing all necessary APKs (where resource ID's might have been reshuffled after code changes?).

like image 2
Oliver Metz Avatar answered Nov 16 '22 15:11

Oliver Metz