web-dev-qa-db-ja.com

androidTestでViewModelを正しくモックする方法

現在、フラグメントのUIユニットテストをいくつか書いていますが、これらの@Testは、オブジェクトのリストが正しく表示されるかどうかを確認することです。これは統合テストではないため、mockViewModel

フラグメントの変数:

class FavoritesFragment : Fragment() {

    private lateinit var adapter: FavoritesAdapter
    private lateinit var viewModel: FavoritesViewModel
    @Inject lateinit var viewModelFactory: FavoritesViewModelFactory

    (...)

コードは次のとおりです。

@MediumTest
@RunWith(AndroidJUnit4::class)
class FavoritesFragmentTest {

    @Rule @JvmField val activityRule = ActivityTestRule(TestFragmentActivity::class.Java, true, true)
    @Rule @JvmField val instantTaskExecutorRule = InstantTaskExecutorRule()

    private val results = MutableLiveData<Resource<List<FavoriteView>>>()
    private val viewModel = mock(FavoritesViewModel::class.Java)

    private lateinit var favoritesFragment: FavoritesFragment

    @Before
    fun setup() {
        favoritesFragment = FavoritesFragment.newInstance()
        activityRule.activity.addFragment(favoritesFragment)
        `when`(viewModel.getFavourites()).thenReturn(results)
    }

    (...)

    // This is the initial part of the test where I intend to Push to the view
    @Test
    fun whenDataComesInItIsCorrectlyDisplayedOnTheList() {
        val resultsList = TestFactoryFavoriteView.generateFavoriteViewList()
        results.postValue(Resource.success(resultsList))

        (...)
    }

ViewModelをモックできましたが、もちろんViewModel内で作成されたFragmentとは異なります。

だから私の質問は本当に、誰かがこれをうまくやったのか、私を助けるかもしれないポインター/リファレンスがありますか?

10
Joaquim Ley

指定した例では、mockitoを使用して、すべてのインスタンスではなく、ビューモデルの特定のインスタンスのモックを返しています。

この作業を行うには、作成した正確なビューモデルモックをフラグメントで使用する必要があります。

ほとんどの場合、これはストアまたはリポジトリから取得されるため、モックをそこに配置できますか?それは、フラグメントロジックでビューモデルの取得をどのように設定するかに大きく依存します。

推奨事項:1)ビューモデルの構築元のデータソースをモックするか、2)fragment.setViewModel()を追加し、テストでのみ使用するものとしてマークします。これは少しいですが、データソースをモックしたくない場合は、この方法でかなり簡単です。

1
Sam Edwards

テストセットアップ内で、Fragmentに注入されるFavoritesViewModelFactoryのテストバージョンを提供する必要があります。

モジュールをTestAppComponentに追加する必要がある場合、次のようなことを行うことができます。

@Module
object TestFavoritesViewModelModule {

    val viewModelFactory: FavoritesViewModelFactory = mock()

    @JvmStatic
    @Provides
    fun provideFavoritesViewModelFactory(): FavoritesViewModelFactory {
        return viewModelFactory
    }
}

その後、テストでMock viewModelを提供できます。

fun setupViewModelFactory() {
    whenever(TestFavoritesViewModelModule.viewModelFactory.create(FavoritesViewModel::class.Java)).thenReturn(viewModel)
}
4
Chris

Daggerによって注入された余分なオブジェクトを使用してこの問題を解決しました。完全な例はこちらにあります: https://github.com/fabioCollini/ArchitectureComponentsDemo

ViewModelFactoryを直接使用していないフラグメントでは、Daggerシングルトンとして定義されているカスタムファクトリを定義しています: https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearch/src/main/Java /it/codingjam/github/ui/search/SearchFragment.kt

次に、テストで DaggerMock を使用して、実際のviewModelではなく常にモックを返すファクトリを使用するこのカスタムファクトリを使用して置き換えます。 https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/ master/uisearchTest/src/androidTest/Java/it/codingjam/github/ui/repo/SearchFragmentTest.kt

3
Fabio Collini

あなたはkotlinとkoin(1.0-beta)を使用しているように見えます。それは私のモックの決定です

@RunWith(AndroidJUnit4::class)
class DashboardFragmentTest : KoinTest {
@Rule
@JvmField
val activityRule = ActivityTestRule(SingleFragmentActivity::class.Java, true, true)
@Rule
@JvmField
val executorRule = TaskExecutorWithIdlingResourceRule()
@Rule
@JvmField
val countingAppExecutors = CountingAppExecutorsRule()

private val testFragment = DashboardFragment()

private lateinit var dashboardViewModel: DashboardViewModel
private lateinit var router: Router

private val devicesSuccess = MutableLiveData<List<Device>>()
private val devicesFailure = MutableLiveData<String>()

@Before
fun setUp() {
    dashboardViewModel = Mockito.mock(DashboardViewModel::class.Java)
    Mockito.`when`(dashboardViewModel.devicesSuccess).thenReturn(devicesSuccess)
    Mockito.`when`(dashboardViewModel.devicesFailure).thenReturn(devicesFailure)
    Mockito.`when`(dashboardViewModel.getDevices()).thenAnswer { _ -> Any() }

    router = Mockito.mock(Router::class.Java)
    Mockito.`when`(router.loginActivity(activityRule.activity)).thenAnswer { _ -> Any() }

    StandAloneContext.loadKoinModules(hsApp + hsViewModel + api + listOf(module {
        single(override = true) { router }
        factory(override = true) { dashboardViewModel } bind ViewModel::class
    }))

    activityRule.activity.setFragment(testFragment)
    EspressoTestUtil.disableProgressBarAnimations(activityRule)
}

@After
fun tearDown() {
    activityRule.finishActivity()
    StandAloneContext.closeKoin()
}

@Test
fun devicesSuccess(){
    val list = listOf(Device(deviceName = "name1Item"), Device(deviceName = "name2"), Device(deviceName = "name3"))
    devicesSuccess.postValue(list)
    onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
    onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name1Item"))))
    onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name2"))))
    onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name3"))))
}

@Test
fun devicesFailure(){
    devicesFailure.postValue("error")
    onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
    Mockito.verify(router, times(1)).loginActivity(testFragment.activity!!)
}

@Test
fun devicesCall() {
    onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
    Mockito.verify(dashboardViewModel, Mockito.times(1)).getDevices()
}

}

3
Alexander

次の方法で、Daggerを使用せずにViewModelや他のオブジェクトを簡単にモックできます。

  1. ViewModelProviderへの呼び出しを再ルーティングできるラッパークラスを作成します。以下は、呼び出しをパラメーターとして渡される実際のViewModelProviderに単に渡すラッパークラスの製品版です。

    class VMProviderInterceptorImpl : VMProviderInterceptor { override fun get(viewModelProvider: ViewModelProvider, x: Class<out ViewModel>): ViewModel {
        return viewModelProvider.get(x)
    }
    

    }

  2. このラッパーオブジェクトのゲッターとセッターをApplicationクラスに追加します。

  3. アクティビティルールでは、アクティビティを起動する前に、get ViewModel呼び出しを実際のviewModelProviderにルーティングせず、代わりに模擬オブジェクトを提供する模擬ラッパーと実際のラッパーを交換します。

これは短剣ほど強力ではありませんが、シンプルさが魅力的だと思います。

0
A.Sanchez.SD