7

更新 29/04/18

重新命名以获得更好的准确性。问题很简单:不能简单地在活动上模拟ViewModel ,因为它们是在活动的 onCreate() 中实例化的。解决这个问题的最佳方法是什么?

一些相关的想法位于此处(未成功尝试实施)

原始问题

使用 Google 的 MVVM GithubBrowserSample 代码库,我正在尝试进行仪器测试以检查加载状态是否会引发进度条。具体来说,是UserFragmentTest.loading() 方法的镜像。这是非常简单的东西,我试图将我的设置与谷歌的设置紧密匹配。

但是我可以看到这是不对的。具体来说,当我明确要求它们不在我的测试函数中时,我可以看到正在调用我的 ViewModel (VM) 中的@Before函数。我正在使用 Kotlin、Dagger2 和架构组件。

当我运行UserFragmentTest.loading()测试时,我可以看到代码在 VM 中确实没有调用任何内容(甚至没有调用构造函数)。BaseActivity然而,即使我要求它返回虚拟数据,我也会调用 VM init 块(设置)和 getUser() 函数。我能看到的唯一主要区别是我的是一个 Activity,而 Google 正在测试一个 Fragment,而 ViewModel 模拟函数使用的是 Niek Haarman 的Mockito-Kotlin库。

登录活动测试.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()))
    }
}

登录活动.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()
        }
    }
}

登录视图模型.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)

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

4

0 回答 0