Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android : How to write a unit test for fragment depending on a viewmodel Live data attribute?

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)
}

}

like image 220
Mohamed Jihed Jaouadi Avatar asked Jan 01 '23 09:01

Mohamed Jihed Jaouadi


1 Answers

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).

Source set structure

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.

like image 112
Masoud Maleki Avatar answered Jan 04 '23 17:01

Masoud Maleki