单元测试 ViewModel - 返回对象中的数据始终为空

问题描述 投票:0回答:1

这是我的视图模型

@HiltViewModel
class MovieDetailsViewModel @Inject constructor(
    private val repository: MovieRepository,
    private val savedStateHandle: SavedStateHandle,
    private val appDispatchers: AppDispatchers,
    ): ViewModel() {

init {
    val movieId = savedStateHandle.get<Int>("id") ?: 568124
    viewModelScope.launch {
        val movieDetails = withContext(appDispatchers.IO) {
            getMovieDetails(movieId)
        }
        movieDetailsState.value = movieDetails
    }
}

    val movieDetailsState: MutableState<MovieDetailsResponse> = mutableStateOf(MovieDetailsResponse())

    private suspend fun getMovieDetails(id: Int): MovieDetailsResponse {
        return repository.getMovie(id)
    }
}

这是电影存储库

class MovieRepository
@Inject constructor(
    private val tmdbWebService: TMDBWebService
) {
    suspend fun getMovie(id: Int): MovieDetailsResponse {
        return tmdbWebService.getMovieDetails(id)
    }
}

应用调度程序

data class AppDispatchers(
    val IO: CoroutineDispatcher = Dispatchers.IO
)

最后,我的视图模型单元测试

@ExperimentalCoroutinesApi
class MovieDetailsViewModelTest {

    @get:Rule
    var mainCoroutineRule = MainCoroutineRule()

    private val movieRepository = mock<MovieRepository> ()
    private val savedStateHandle = mock<SavedStateHandle> ()

    private lateinit var viewModel: MovieDetailsViewModel

    private val testDispatcher = AppDispatchers(
        IO = TestCoroutineDispatcher()
    )

    @Before
    fun setup() {
        viewModel = MovieDetailsViewModel(movieRepository, savedStateHandle, testDispatcher)
    }

@Test
fun `Loading state works`() = runBlockingTest {
    coEvery { savedStateHandle.get<Int>("id") } returns 5
    coEvery { movieRepository.getMovie(5) } returns getDummyMovieDetailData(5)

    viewModel = MovieDetailsViewModel(movieRepository, savedStateHandle, testDispatcher)
    Assert.assertEquals(getDummyMovieDetailData(5), viewModel.movieDetailsState.value)
}

    private fun getDummyMovieDetailData(id: Int): MovieDetailsResponse {
        return MovieDetailsResponse(
            id = id,
            budget = 50000,
            title = "Encanto",
            overview = "The tale of an extraordinary family, the Madrigals, who live hidden in the mountains of Colombia, in a magical house, in a vibrant town, in a wondrous, charmed place called an Encanto. The magic of the Encanto has blessed every child in the family—every child except one, Mirabel. But when she discovers that the magic surrounding the Encanto is in danger, Mirabel decides that she, the only ordinary Madrigal, might just be her exceptional family's last hope.",
            releaseDate = "2021-10-13",
            revenue = 253000000,
            runtime = 102,
            status = "released",
            tagline = "There's a little magic in all of us...almost all of us.",
            voteAverage = 7.637,
            genres = getDummyGenreData(),
            productionCompanies = getDummyProductionCompanyData()
        )
    }

    private fun getDummyGenreData(): ArrayList<Genres> {
        return arrayListOf(
            Genres(16, "Animation"),
            Genres(35, "Comedy"),
            Genres(10751, "Family"),
            Genres(14, "Fantasy"),
        )
    }

    private fun getDummyProductionCompanyData(): ArrayList<ProductionCompanies> {
        return arrayListOf(
            ProductionCompanies(
                6125,
                "/tzsMJBJZINu7GHzrpYzpReWhh66.png",
                "Walt Disney Animation Studios",
                "US"
            ),
            ProductionCompanies(
                2,
                "/wdrCwmRnLFJhEoH8GSfymY85KHT.png",
                "Walt Disney Pictures",
                "US"
            )
        )
    }
}

我遇到的问题是预期的数据与我的本地对象不匹配,返回的对象中返回的数据均为空。通过研究,我的理解是,这可能与这些在不同线程上运行有关,因此我添加了 AppDispatchers 类。

返回的错误是:

无法调用“androidx.compose.runtime.MutableState.setValue(Object)”,因为“com.someone.xmovies.ui.detailsscreen.MovieDetailsViewModel.getMovieDetailsState()”的返回值为 null 在 com.someone.xmovies.ui.detailsscreen.MovieDetailsViewModel$1.invokeSuspend(MovieDetailsViewModel.kt:26)

数据对比:

Expected :MovieDetailsResponse(adult=null, backdropPath=null, belongsToCollection=BelongsToCollection(id=null, name=null, posterPath=null, backdropPath=null), budget=50000, genres=[Genres(id=16, name=Animation), Genres(id=35, name=Comedy), Genres(id=10751, nam ...

Actual   :MovieDetailsResponse(adult=null, backdropPath=null, belongsToCollection=BelongsToCollection(id=null, name=null, posterPath=null, backdropPath=null), budget=null, genres=[], homepage=null, id=null, imdbId=null, originalLanguage=null, originalTitle=nul ...

我曾尝试使用

Mockk
来测试这一点,但我知道这是不可能的,因此我使用
Mockito
来代替。我当然也使用
Hilt
以及 DI。

我的build.gradle文件:

// For local unit tests
testImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
kaptTest 'com.google.dagger:hilt-compiler:2.46.1'

// Mockito
testImplementation 'org.mockito.kotlin:mockito-kotlin:4.1.0'
testImplementation 'org.mockito:mockito-core:3.12.4'
testImplementation 'org.mockito:mockito-inline:2.13.0'
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2"

//Mockk
testImplementation "io.mockk:mockk:1.13.5"
testImplementation "io.mockk:mockk-android:1.13.5"
testImplementation "io.mockk:mockk-agent:1.13.5"
androidTestImplementation "io.mockk:mockk-android:1.13.5"
androidTestImplementation "io.mockk:mockk-agent:1.13.5"

如果您能够帮助我理解并解决这个问题,我将非常感激!

从进一步的测试中我可以看到,在 viewModel 类中,movieId 始终是默认的 568124,而不是模拟应返回的值“5”。

即使我强制默认为 5,返回的

movieDetails
仍然为空。

android unit-testing mockito coroutine mockk
1个回答
0
投票

我已经成功解决了这个问题。分享修改后的代码。希望它对其他人有帮助。

MovieDetailsViewModelTest

@get:Rule
var mainCoroutineRule = MainCoroutineRule()

private val movieRepository = mockk<MovieRepository> (relaxed = true)
private val savedStateHandle = mockk<SavedStateHandle> (relaxed = true)

private lateinit var viewModel: MovieDetailsViewModel

private val testDispatcher = AppDispatchers(
    IO = TestCoroutineDispatcher()
)

@Before
fun setup() {
    every { savedStateHandle.get<Int>("id") } returns 5
    viewModel = MovieDetailsViewModel(movieRepository, savedStateHandle, testDispatcher)
}

@Test
fun `Loading state works`() = runBlockingTest {
    every { savedStateHandle["id"] = 5 }
    coEvery { movieRepository.getMovie(5) } returns getDummyMovieDetailData(5)

    viewModel = MovieDetailsViewModel(movieRepository, savedStateHandle, testDispatcher)
    Assert.assertEquals(getDummyMovieDetailData(5), viewModel.movieDetailsState.value)
}

主要的变化是添加

relaxed = true
行来表明这是一个轻松的模拟,但我可以覆盖一些方法。

此外,为了解决

SaveStateHandle
值未返回的问题,我修改了处理方式。老实说,我不清楚为什么我需要
setup
中的行和我的测试中的行,因为在我看来,测试中的行与另一行相同。 :/

最后的更改是

SaveStateHandle
调用不应位于
CoEvery
包装器内,因为它不在协同例程内。

这个网站对我很有帮助: https://developersbreach.com/savedstatehandle-viewmodel-android/

© www.soinside.com 2019 - 2024. All rights reserved.