我一直在研究旧版 Android 应用程序,尝试为其添加测试和适当的架构。该应用程序有一个主要LaunchActivity
它在启动时运行一系列检查。最初,该活动使用 Dagger 来“注入依赖项”,活动将使用它来运行检查,但效果相当糟糕。
我转向 MVVM,这样我就可以单独测试视图模型,无需使用仪器,并且只需要为 UI 测试注入模拟视图模型。我跟着本文 https://android.jlelse.eu/7-steps-to-implement-dagger-2-in-android-dabc16715a3a介绍这些更改,包括切换到使用新的 Dagger Android 方法,例如AndroidInjection.inject
.
我希望测试能够尽可能地指导任何更改,因此当我的基本架构正常工作时,我转而编写 UI 测试。现在,事实证明,必须使用 Dagger 将模拟视图模型注入到活动中是一项艰巨的任务,但我认为我已经找到了一个可行的解决方案。
我已经在使用了TestApp
与自定义仪器运行器一起使用DexOpener
,我将其更改为也实施HasActivityInjector
,很像实际的习俗App
对于我的应用程序(两者都扩展Application
).
对于 Dagger,我创建了单独的模块和一个用于测试的组件:
测试应用组件
@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)
}
测试视图模型模块
@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
}
}
测试AndroidContributor模块
@Module
abstract class TestAndroidContributorModule {
@ContributesAndroidInjector
abstract fun contributeLaunchActivity(): LaunchActivity
}
然后,在LaunchActivityTest
, 我有:
@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()))
}
}
结果是视图模型被正确模拟,所以其他一切都应该工作......但是,当运行 Espresso 测试时,尽管测试显示它们已经通过,但有一个奇怪的堆栈跟踪,其中(通过)视图断言应该是。
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)
中的声明LaunchActivityTest
错误追踪到的地方是:
onView(withId(R.id.launch_page_slider)).check(matches(isDisplayed()))
我不明白为什么测试显示此错误。我知道这与 Dagger 有关,因为如果我注释掉构建DaggerTestAppComponent
,没有问题。但是,如果不使用此测试组件,我不确定如何将模拟视图模型注入到活动中。有些东西导致 Dagger 和 Espresso 不能很好地发挥作用,我认为与此有关DaggerBaseLayerComponent
在堆栈跟踪中。但我没有别的事了。
我目前唯一的“解决方案”是切换到 Fragment 而不是 Activity,这样我就可以在测试中完全跳过 Dagger 的需要并遵循这个样本 https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample,但我真的很困惑为什么会遇到这个问题。我将非常感谢任何帮助找出原因的帮助。