Android:如何根据视图模型实时数据属性为片段编写单元测试?

J.M*_*M.J 4 android unit-testing kotlin android-livedata android-viewmodel

我的片段 UI 中有一个列表视图,它的元素设置取决于来自视图模型 LiveData 属性的值的状态。

我想为片段创建工具测试,该片段包含与该属性的值集相关的 3 个场景测试用例,但我不知道从哪里开始。

我的代码应该如下所示:

class MyViewModel : ViewModel() {
var status = MutableLiveData("")
}


class MyFragment : Fragment() {

private lateinit var myViewModel: MyViewModel

private lateinit var myListView: ListView

override fun onAttach(context: Context) {
    AndroidSupportInjection.inject(this)
    super.onAttach(context)

    myViewModel =
        ViewModelProviders.of(this, ViewModelProvider.Factory).get(MyViewModel::class.java)
}

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {

    when (myViewModel?.status) {

        "status1":
            setListContent(items1)

        "status1":
            setListContent(items2)

        "status1":
            setListContent(items3)

        else
            setListContent
        (items1)

    }
}

private fun setListContent(itemsList: List<?>) {
    myListView.adapter = MyCustomadapter(context!!, itemsList)
}
Run Code Online (Sandbox Code Playgroud)

}

Mas*_*eki 8

首先,您应该将片段本身的编写测试与视图模型和实时数据的测试分开。

由于您想根据 viewmodel Live data片段编写测试,那么我认为一个解决方案是模拟视图模型(或视图模型所依赖的存储库)并使用 FragmentScenario 启动您的片段并对其进行测试。就像在这个codelab 中所做的一样。

编辑:基于您提供的新代码

首先,我对您的代码进行了一些更改,以使其可运行和可测试(此代码只是一个可运行的代码,仅用于测试,而不是格式良好且编写良好的代码):

我的片段:

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ListView
import androidx.annotation.VisibleForTesting
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider.Factory
import androidx.lifecycle.ViewModelProviders

class MyFragment : Fragment() {

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    lateinit var myViewModel: MyViewModel

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    lateinit var myListView: ListView

    override fun onAttach(context: Context) {
        super.onAttach(context)

        val FACTORY = object : Factory {
            override fun <T : ViewModel> create(modelClass: Class<T>): T {
                return MyViewModel() as T
            }
        }
        myViewModel =
            ViewModelProviders.of(this, FACTORY).get(MyViewModel::class.java)
        myListView = ListView(context)
        myListView.adapter = MyCustomadapter(context, listOf("a", "b", "c"))

    }

    val items1 = listOf("a", "b", "c")
    val items2 = listOf("1", "2")
    val items3 = listOf("a1", "a2", "a3")

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        when (myViewModel.status.value) {

            "status1" ->
                setListContent(items1)

            "status2" ->
                setListContent(items2)

            "status3" ->
                setListContent(items3)

            else -> setListContent(items1)
        }
        return View(context)
    }

    private fun setListContent(itemsList: List<String>) {
        myListView.adapter = MyCustomadapter(context!!, itemsList)
    }
}
Run Code Online (Sandbox Code Playgroud)

我的自定义适配器:


import android.content.Context
import android.database.DataSetObserver
import android.view.View
import android.view.ViewGroup
import android.widget.ListAdapter

class MyCustomadapter(private val context: Context, private val itemsList: List<String>) : ListAdapter {
    override fun isEmpty(): Boolean {
        return itemsList.isEmpty()
    }

    override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View {
        return View(context)
    }

    override fun registerDataSetObserver(p0: DataSetObserver?) {

    }

    override fun getItemViewType(p0: Int): Int {
        return 1
    }

    override fun getItem(p0: Int): Any {
        return itemsList[p0]
    }

    override fun getViewTypeCount(): Int {
        return 3
    }

    override fun isEnabled(p0: Int): Boolean {
        return true
    }

    override fun getItemId(p0: Int): Long {
        return 0
    }

    override fun hasStableIds(): Boolean {
        return true
    }

    override fun areAllItemsEnabled(): Boolean {
        return true
    }

    override fun unregisterDataSetObserver(p0: DataSetObserver?) {

    }

    override fun getCount(): Int {
        return itemsList.size
    }

}
Run Code Online (Sandbox Code Playgroud)

我的视图模型:

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MyViewModel : ViewModel() {
    var status = MutableLiveData<String>()
}
Run Code Online (Sandbox Code Playgroud)

在上面的代码中,我使用了 @VisibleForTesting 注释来测试私有字段。[但我建议不要这样做,而是使用公共方法或 UI 组件来测试代码行为。由于您没有在此处提供任何 UI 组件,因此我没有其他简单的选择来测试您的代码]。

现在我们向应用模块的 build.gradle 添加依赖项:

testImplementation 'junit:junit:4.12'
debugImplementation 'androidx.fragment:fragment-testing:1.1.0'
testImplementation 'androidx.test.ext:junit:1.1.1'
testImplementation 'org.robolectric:robolectric:4.3.1'
testImplementation 'androidx.arch.core:core-testing:2.1.0'
Run Code Online (Sandbox Code Playgroud)

junit : 用于纯单元测试 ['pure' 意味着你不能在你的 junit 测试中使用与 android 相关的代码]。我们总是需要这个库来编写我们的 android 测试。

片段测试:用于使用FragmentScenario。为了避免 robolectric 风格问题,我们使用“debugImplementation”而不是“testImplementation”。

androidx.test.ext:junit:用于使用 AndroidJUnit4 测试运行器。

robolectric:我们在这里使用 robolectric 在 JVM 上运行 android 仪器测试 - 本地(而不是在 android 模拟器或物理设备上运行)。

androidx.arch.core:core-testing:我们用它来测试实时数据

为了能够在 robolectric 中使用 android 资源,我们需要向 app build.gradle 添加一个测试选项:

android {
    ...
    testOptions {
        unitTests {
            includeAndroidResources = true
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,我们编写一个简单的测试

[将此测试放在“测试”源集中而不是“androidTest”中。您也可以通过在 android studio 中按 Ctrl + Shift + T 为您的代码创建测试文件,或者通过右键单击类名并按 generate>Test... 并选择“test”源集]:

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.fragment.app.testing.launchFragment
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MyFragmentTest {
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Test
    fun changingViewModelValue_ShouldSetListViewItems() {
        val scenario = launchFragment<MyFragment>()
        scenario.onFragment { fragment ->
            fragment.myViewModel.status.value = "status1"
            assert(fragment.myListView.adapter.getItem(0) == "a")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在上面的测试中,我们通过设置实时数据值来测试设置列表视图项。该“InstantTaskExecutorRule”是对保证实时数据值将在可预见的方式进行测试(如解释在这里)。

源集结构

如果你想与像咖啡或其他库的库来测试你的UI组件(如屏幕显示测试项目)首先添加的依赖于摇篮,然后改变launchFragment<MyFragment>()launchFragmentInContainer<MyFragment>()所描述这里