问题
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.
来源:https://stackoverflow.com/questions/58061693/espresso-generating-filenotfoundexception-when-used-with-dagger