MVVM活动中的模拟ViewModel

时间:2018-04-20 14:35:57

标签: kotlin mockito android-espresso android-architecture-components android-mvvm

更新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)

修改 看起来这里的问题是虚拟机未被正确模拟。有几个相关问题herehere存在非常相似的问题。注入的VM工厂首先在基础活动的onCreate中使用,但活动需要存在才能覆盖它以使用我们的模拟VM。

0 个答案:

没有答案