问题
I have a listview in my fragment UI that its elements set depend on status of a value that come from a viewmodel LiveData attribute.
I want to create instrumental test for the fragment which englobes 3 scenarios test case related to the value set of that attribute and I don't where to start.
My code should kind look like below :
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)
}
}
回答1:
First you should separate writing tests for fragment itself and tests for view model and live data.
Since you want to write test for fragment depending on a viewmodel Live data, then I think a solution is to mock the view model (or the repository that view model depends on) and launch your fragment using FragmentScenario and test it. Like what is done in this codelab.
Edit: based on your new provided code
First I do some changes in your code to make it runnable and testable (This code is just a code that runs and is just for testing and isn't a well-formed and well-written code):
MyFragment:
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)
}
}
MyCustomadapter:
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
}
}
MyViewModel:
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class MyViewModel : ViewModel() {
var status = MutableLiveData<String>()
}
In the above code, I used the @VisibleForTesting annotation for able to testing private fields. [But I recommend to not do like this, and instead, use public methods or the UI components to test the code behaviour. Since you have not provided any UI component here, I have no other simple choice for testing your code].
Now we add dependencies to app modules' 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'
junit: is for pure unit testing ['pure' means that you can't use android related code in your junit tests]. We always need this library for writing our android tests.
fragment-testing: for using FragmentScenario. For avoid robolectric style problem we use 'debugImplementation' instead of 'testImplementation'.
androidx.test.ext:junit: is for using AndroidJUnit4 test runner.
robolectric: we use robolectric here for running android instrumentation tests on JVM - locally (instead of running on android emulator or physical device).
androidx.arch.core:core-testing: we use this for testing live data
For able to use android resources in robolectric we need to add a test option to app build.gradle:
android {
...
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
And finally, we write a simple test:
[put this test in "test" source set and not in "androidTest". Also you can create test file for your code by pressing Ctrl + Shift + T in android studio, or by right clicking on class name and pressing generate>Test... and selecting 'test' source set]:
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")
}
}
}
In the above test, we tested setting list view items by setting live data value. The 'InstantTaskExecutorRule' is for assuring that live data value will be tested in predictable way (As explained here).
If you want to test your UI components (like testing displayed items in screen) with libraries like Espresso or other libraries first add its dependency to gradle and then change the launchFragment<MyFragment>()
to launchFragmentInContainer<MyFragment>()
as described here.
来源:https://stackoverflow.com/questions/59666993/android-how-to-write-a-unit-test-for-fragment-depending-on-a-viewmodel-live-da