更新29/04/18
重新命名以获得更高的准确性。问题很简单: ViewModels 不能简单地模仿活动,因为它们是在Acitivity的onCreate()中实例化的。最好的方法是什么?
一些related ideas located here(未成功尝试实施)
原始问题
使用Google的MVVM GithubBrowserSample代码库,我正在尝试进行一个仪器测试,以检查加载状态是否会引发进度条。具体来说,是the UserFragmentTest.loading() method的镜像。这是非常简单的事情,我试图将我的设置与谷歌的设置完全匹配。
但我觉得它不对。具体来说,当我明确要求它们不在我的测试@Before
函数中时,我可以看到我的ViewModel(VM)中的函数被调用。我正在使用Kotlin,Dagger2和架构组件。
当我运行UserFragmentTest.loading()
测试时,我可以看到代码确实在VM中没有调用任何内容(甚至不是构造函数)。然而,我会调用VM初始化块(BaseActivity
设置)和getUser()函数,即使我要求它返回虚拟数据。我能看到的唯一主要区别是我的是一个Activity而Google正在测试一个Fragment,而ViewModel模拟函数正在使用Niek Haarman的Mockito-Kotlin库。
LoginActivityTest.kt
@RunWith(AndroidJUnit4::class)
class LoginActivityTest {
private val email = "***********@gmail.com"
private val password = "123456"
@Suppress("MemberVisibilityCanBePrivate")
@get:Rule
val activityRule = ActivityTestRule(LoginActivity::class.java)
private lateinit var viewModel:LoginViewModel
private val userData = MutableLiveData<Resource<User>>()
@Before
@Throws(Throwable::class)
fun init() {
EspressoTestUtil.disableProgressBarAnimations(activityRule)
Log.d("LoginTest Init", "vm mocked....")
viewModel = mock()
`when`(viewModel.getUser()).thenReturn(userData)
doNothing().`when`(viewModel).setLogin(anyString(), anyString())
activityRule.activity.viewModelFactory = ViewModelUtil.createFor(viewModel)
}
@Test
fun loading(){
//Given: our login event has been kicked off
onView(withId(R.id.etEmail)).perform(replaceText(email))
onView(withId(R.id.etPassword)).perform(replaceText(password))
Log.d("LoginTest", "Posting loading value ")
userData.postValue(Resource.loading(null))
onView(withId(R.id.progress_text_view)).check(matches(isDisplayed()))
}
}
LoginActivity.kt
class LoginActivity : BaseActivity<ActivityLoginBinding, LoginViewModel>(), LoginNavigator {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
internal set
override val viewModel: LoginViewModel
get() = ViewModelProviders.of(this, viewModelFactory).get(LoginViewModel::class.java)
private lateinit var activityLoginBinding: ActivityLoginBinding
override val bindingVariable: Int
get() = BR.viewModel
override val layoutId: Int
get() = R.layout.activity_login
override val progressTextView: TextView?
get() = activityLoginBinding.progressInclude?.progressTextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityLoginBinding = viewDataBinding
viewModel.navigator = this
Log.d("LoginActivity", "1 - onCreate, observing vm.getUser changes")
viewModel.getUser().observe(this, Observer {
Log.d("LoginActivity", "4 - onCreate, user change detected")
activityLoginBinding.userResource = it
it?.let {
Log.d("LoginActivity", "5 - onCreate, user change, not null: " + it)
viewModel.login(it)
}
})
}
override fun login() {
Log.d("LoginActivity", "2 - activity login() called, getting email + pass for vm")
val email = activityLoginBinding.etEmail.text.toString()
val password = activityLoginBinding.etPassword.text.toString()
if (viewModel.isEmailAndPasswordValid(email, password)) {
Log.d("LoginActivity", "3, setting email and pass on vm")
viewModel.setLogin(password, email)
} else {
Toast.makeText(this, "invalid email or password", Toast.LENGTH_SHORT).show()
}
}
}
LoginViewModel.kt
@OpenForTesting
class LoginViewModel @Inject constructor(loginInteractor: LoginInteractor,
schedulerProvider: SchedulerProvider)
: BaseViewModel<LoginNavigator, LoginInteractor>(loginInteractor, schedulerProvider) {
@VisibleForTesting
final var loginCredentials: MutableLiveData<Pair<String, String>> = MutableLiveData()
set(value) {
if (value.value === field.value) return
field = value
}
final var user: LiveData<Resource<User>>
init {
Log.d("LoginVM", "* - Init block, shouldn't be here...")
user = Transformations.switchMap(loginCredentials) {
Log.d("LoginVM", "user switchmap returning " + it.first)
if (it.first.isBlank() || it.second.isBlank())
AbsentLiveData.create()
else
interactor.callServerLoginRepo(it.first, it.second)
}
}
@VisibleForTesting
fun getUser(): LiveData<Resource<User>> {
Log.d("LoginVM", "* - calling get User, shouldn't be here: " + user.value)
return user
}
@VisibleForTesting
fun setLogin(password: String, email: String) {
Log.d("LoginVM", "* - calling setLogin, shouldn't be here")
loginCredentials.value = Pair(password, email)
}
fun onServerLoginClick() {
navigator?.login()
}
override fun onUnknownError(message: String) {
navigator?.handleError(message)
}
fun isEmailAndPasswordValid(email: String, password: String): Boolean {
return email.isValidEmail() && password.isValidPassword(AppConstants.MINIMUM_PASSWORD_LENGTH)
}
fun login(resource: Resource<User>) {
Log.d("LoginVM", "* - vm login resource: " + resource)
if (resource.status == Status.SUCCESS && resource.data != null) {
//Login success
}
else if (resource.status == Status.ERROR){
resource.message?.let {
navigator?.handleError(it)
}
}
}
}
运行测试后的Logcat:
04-20 14:25:45.769 1635-1650/? I/TestRunner: started: loading(app.core.sdk.ui.login.LoginActivityTest)
04-20 14:25:45.771 1635-1650/? I/ActivityTestRule: Launching activity: ComponentInfo{app.core.sdk/app.core.sdk.ui.login.LoginActivity}
04-20 14:25:45.826 1635-1635/? D/LifecycleMonitor: Lifecycle status change: app.core.sdk.ui.login.LoginActivity@9ab279d in: PRE_ON_CREATE
04-20 14:25:45.931 1635-1635/? D/LoginVM: * - Init block, shouldn't be here...
04-20 14:25:45.932 1635-1635/? D/LoginActivity: 1 - onCreate, observing vm.getUser changes
04-20 14:25:45.932 1635-1635/? D/LoginVM: * - calling get User, shouldn't be here: null
04-20 14:25:45.935 1635-1635/? D/LifecycleMonitor: Lifecycle status change: app.core.sdk.ui.login.LoginActivity@9ab279d in: CREATED
04-20 14:25:45.936 1635-1635/? D/LifecycleMonitor: Lifecycle status change: app.core.sdk.ui.login.LoginActivity@9ab279d in: STARTED
04-20 14:25:45.937 1635-1635/? D/LifecycleMonitor: Lifecycle status change: app.core.sdk.ui.login.LoginActivity@9ab279d in: RESUMED
04-20 14:25:46.518 1635-1650/? D/LoginTest Init: vm mocked....
04-20 14:25:47.290 1635-1650/app.core.sdk D/LoginTest: Posting loading value
04-20 14:25:47.317 1635-1635/app.core.sdk D/LifecycleMonitor: Lifecycle status change: app.core.sdk.ui.login.LoginActivity@9ab279d in: PAUSED
04-20 14:25:47.401 1635-1650/app.core.sdk I/TestRunner: failed: loading(app.core.sdk.ui.login.LoginActivityTest)
修改 看起来这里的问题是虚拟机未被正确模拟。有几个相关问题here和here存在非常相似的问题。注入的VM工厂首先在基础活动的onCreate中使用,但活动需要存在才能覆盖它以使用我们的模拟VM。