Example of Hilt + test run Android - android

Can you please give me steps how to setup project to be able to run unit test with hilt and mockito.
Here is my code of the test
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
class CustomDialogViewModelTest {
private val prefsManager : PrefsManager = mock()
private lateinit var viewModel : CustomDialogViewModel
#Before
fun setUp() {
viewModel = CustomDialogViewModel(prefsManager)
}
#Test
fun testFun(){
viewModel.getToDoItemFromPrefs()
}
}
And do I need to setup a separate module for tests?

Related

Android ViewModel with StateFlow - testing issue. Test never waits for next value

I have search view model like this.
searchPoiUseCase doing requests to Room DB. For testing purposes i am using Room.inMemoryDatabaseBuilder.
#HiltViewModel
class SearchVm #Inject constructor(
private val searchPoiUseCase: SearchPoiUseCase
) : ViewModel() {
private val queryState = MutableStateFlow("")
#OptIn(FlowPreview::class)
val searchScreenState = queryState
.filter { it.isNotEmpty() }
.debounce(500)
.distinctUntilChanged()
.map { query -> searchPoiUseCase(SearchPoiUseCase.Params(query)) }
.map { result ->
if (result.isEmpty()) SearchScreenUiState.NothingFound
else SearchScreenUiState.SearchResult(result.map { it.toListUiModel() })
}
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
SearchScreenUiState.None
)
fun onSearch(query: String) {
queryState.value = query
}
}
On the device this logic works perfectly fine. But i can't succeed with Unit Testing this logic.
Here is my unit test:
#OptIn(ExperimentalCoroutinesApi::class)
#HiltAndroidTest
#Config(application = HiltTestApplication::class)
#RunWith(RobolectricTestRunner::class)
class SearchViewModelTest {
#get:Rule
var hiltRule = HiltAndroidRule(this)
#Inject
lateinit var searchUseCase: SearchPoiUseCase
lateinit var SUT: SearchVm
#Before
fun setup() {
hiltRule.inject()
SUT = SearchVm(searchUseCase)
Dispatchers.setMain(UnconfinedTestDispatcher())
}
#After
fun teardown() {
Dispatchers.resetMain()
}
#Test
fun `test search view model`() = runTest {
val collectJob = launch { SUT.searchScreenState.collect() }
assertEquals(SearchScreenUiState.None, SUT.searchScreenState.value)
SUT.onSearch("Query")
assertEquals(SearchScreenUiState.NothingFound, SUT.searchScreenState.value)
collectJob.cancel()
}
}
The second assertion always failed.
Am i missing something? Thanks in advance!
UPDATED
Thanks to Ibrahim Disouki
His solution working for me with one change
#Test
fun `test search view model`() = runTest {
whenever(searchUseCase(SearchPoiUseCase.Params("Query"))).thenReturn(emptyList()) // here you can create another test case when return valid data
assertEquals(SearchScreenUiState.None, SUT.searchScreenState.value)
val job = launch {
SUT.searchScreenState.collect() //now it should work
}
SUT.onSearch("Query")
advanceTimeBy(500) // This is required in order to bypass debounce(500)
runCurrent() // Run any pending tasks at the current virtual time, according to the testScheduler.
assertEquals(SearchScreenUiState.NothingFound, SUT.searchScreenState.value)
job.cancel()
}
Please check the following references:
unit-tests
unit-test-the-new-kotlin-coroutine-stateflow
Also, your view model can be run with the regular JUnit test runner as it does not contain any specific Android framework dependencies.
Check my working and tested version of your unit test:
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.whenever
#OptIn(ExperimentalCoroutinesApi::class)
#RunWith(MockitoJUnitRunner::class)
class SearchViewModelTest {
#Mock
private lateinit var searchUseCase: SearchPoiUseCase
lateinit var SUT: SearchVm
#Before
fun setup() {
Dispatchers.setMain(UnconfinedTestDispatcher())
SUT = SearchVm(searchUseCase)
}
#After
fun teardown() {
Dispatchers.resetMain()
}
#Test
fun `test search view model`() = runTest {
whenever(searchUseCase(SearchPoiUseCase.Params("Query"))).thenReturn(emptyList()) // here you can create another test case when return valid data
assertEquals(SearchScreenUiState.None, SUT.searchScreenState.value)
val job = launch {
SUT.searchScreenState.collect() //now it should work
}
SUT.onSearch("Query")
runCurrent() // Run any pending tasks at the current virtual time, according to the testScheduler.
assertEquals(SearchScreenUiState.NothingFound, SUT.searchScreenState.value)
job.cancel()
}
}
Another important thing from mocking the SearchPoiUseCase is to manipulating its result to be able to test more cases for example:
Return an empty list
Return a list of results.
etc...

testing Repository class that takes DAO parameter

I am learning Kotlin to build a note app. I have created a repository class as shown below which takes a Dao parameter. For now, the source of data is just Dao but in the tutorial I am following, it calls an API class as well.
What I want to know is how do I test a repository classes logic?
import androidx.lifecycle.LiveData
import com.example.lastnotetakingapp.db.daos.NoteDao
import com.example.lastnotetakingapp.db.models.Note
class NotesRepo(private val notesDao: NoteDao) {
val allNotes: LiveData<List<Note>> = notesDao.getAllNotes()
suspend fun addNewNote(note: Note): Long {
return notesDao.addNewNote(note)
}
}
My test which is passing but it is 100% identical to the way a Dao would be tested except I use repo object, which made wonder if I am doing it right or not:
Is mocking the Database/DAO possible so I can spy on them to make sure they are called and all?
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.lastnotetakingapp.db.NoteDB
import com.example.lastnotetakingapp.db.daos.NoteDao
import com.example.lastnotetakingapp.db.models.Note
import com.example.lastnotetakingapp.testHelpers.getOrAwaitValue
import com.google.common.truth.Truth
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
#RunWith(AndroidJUnit4::class)
class NotesRepoTest {
private lateinit var dao: NoteDao
private lateinit var db: NoteDB
private lateinit var notesRepo: NotesRepo
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#Before
fun setUp(){
db = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
NoteDB::class.java,
).allowMainThreadQueries().build()
dao= db.noteDao
notesRepo = NotesRepo(dao)
}
#After
fun tearDown(){
db.close()
}
#Test
fun saveNotesTest(): Unit = runBlocking{
val note = Note(0, "tupac", "content", 0)
val id : Long = notesRepo.addNewNote(note)
Truth.assertThat(id).isEqualTo(1)
val notes = notesRepo.allNotes.getOrAwaitValue()
val noteOne: Note? = notes?.get(0)
Truth.assertThat(notes?.size).isEqualTo(1)
Truth.assertThat(noteOne?.title).isEqualTo(note.title)
Truth.assertThat(noteOne?.content).isEqualTo(note.content)
Truth.assertThat(noteOne?.viewed).isEqualTo(false)
}
}

How to run an integration test for the Repository layer of an Android app with Firestore as a backend using Flow

I am currently trying to write an integration test for my repository layer that tests if I call a method, getExercises(), then it returns List<Exercise>, provided that the data is loaded into the local Firestore emulator ahead of time.
So far I got the local Firestore emulator to switch on and off at the beginning/end of a test run, respectively. I am able to populate my data into Firestore, and see the data in the local Firestore emulator via the web UI.
My problem is that my test assertion times out because the Task (an asynchronous construct the Firestore library uses), blocks the thread at the await() part in the repository method.
Test
package com.example.fitness.data
import androidx.test.ext.junit.runners.AndroidJUnit4
import app.cash.turbine.test
import com.example.fitness.Constants.EXERCISES_REF
import com.example.fitness.FirebaseEmulatorTest
import com.google.android.gms.tasks.Tasks
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
#HiltAndroidTest
#RunWith(AndroidJUnit4::class)
class ExerciseRepositoryTest : FirebaseEmulatorTest() {
#get:Rule
var hiltRule = HiltAndroidRule(this)
#Inject
lateinit var subject: ExerciseRepository
#Before
fun setup() {
hiltRule.inject()
}
#ExperimentalTime
#Test
fun `#getExercises returns a flow of exercises`() = runBlocking {
val exercises = mutableListOf<Exercise>().apply {
add(Exercise("a", "pushups"))
add(Exercise("b", "pull-ups"))
add(Exercise("c", "sit-ups"))
}
runBlocking(Dispatchers.IO) {
val task1 = firestoreInstance.collection(EXERCISES_REF).add(exercises.first())
val task2 = firestoreInstance.collection(EXERCISES_REF).add(exercises[1])
val task3 = firestoreInstance.collection(EXERCISES_REF).add(exercises.last())
Tasks.await(task1)
Tasks.await(task2)
Tasks.await(task3)
println("Done with tasks: task1: ${task1.isComplete}. task2: ${task2.isComplete}. task3: ${task3.isComplete}.")
}
println("About to get exercises")
subject.getExercises().test(timeout = Duration.seconds(5)) {
println("test body")
assertThat(awaitItem().size, `is`(4)) // Just checking that it passes for the right reasons first. This number should be 3
}
}
}
Repository (System under test)
package com.example.fitness.data
import com.example.fitness.Constants.EXERCISES_REF
import com.google.firebase.firestore.CollectionReference
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
#Singleton
class ExerciseRepository #Inject constructor(
#Named(EXERCISES_REF) private val exerciseCollRef: CollectionReference
) {
fun getExercises() = flow<List<Exercise>> {
println("beginning of searchForExercise")
val exercises = exerciseCollRef.limit(5).get().await() // NEVER FINISHES!!
println("Exercise count: ${exercises.documents}")
emit(exercises.toObjects(Exercise::class.java))
}
}
The output of this results in:
Done with tasks: task1: true. task2: true. task3: true.
About to search for exercises
beginning of searchForExercise
test body
Timed out waiting for 5000 ms
kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 5000 ms
"Exercise count: 3" message never prints!
Note: I am using Robolectric 4.6.1, kotlinx-coroutines-playservices (1.5.0) to provide the await() extension function, and the Turbine testing library for flow assertions (0.6.1)
Perhaps of relevance is a superclass this test inherits that sets the main dispatcher to a test dispatcher.
package com.example.fitness
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Before
import org.junit.Rule
abstract class CoroutineTest {
#Rule
#JvmField
val rule = InstantTaskExecutorRule()
protected val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testDispatcher)
#Before
fun setupViewModelScope() {
Dispatchers.setMain(testDispatcher)
}
#After
fun cleanupViewModelScope() {
Dispatchers.resetMain()
}
#After
fun cleanupCoroutines() {
testDispatcher.cleanupTestCoroutines()
testDispatcher.resumeDispatcher()
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest(block)
}
Any help here would be greatly appreciate.
Edit
I have opened an issue with the kotlin extensions team to get more visibility on how to go about testing this, including a repo demonstrating the problem.
This problem has been resolved in a new version of the kotlinx-coroutines package (1.6.0-RC). See my github compare across branches. Tests now pass as expected with this version.

LiveData (backed by Flow) in Test is reflecting old value

I am trying to write a test for my View Model that verifies when I call setFirstTime, the state of the view model contains the updated value for firstTime set to false.
The UserPreferencesRepository provides a Flow of the preferences to the viewmodel, which exposes them as LiveData (using asLiveData extension).
Here is my test I am having trouble with:
MainViewModelTest.kt
package com.example.fitness.main
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.fitness.MainCoroutineRule
import com.example.fitness.data.UserPreferencesRepository
import com.example.fitness.getOrAwaitValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
#HiltAndroidTest
#RunWith(AndroidJUnit4::class)
class MainViewModelTest {
#get:Rule
var hiltRule = HiltAndroidRule(this)
#get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
#get:Rule
#ExperimentalCoroutinesApi
var mainCoroutineRule = MainCoroutineRule()
private lateinit var mainViewModel: MainViewModel
#Inject
lateinit var userPreferencesRepository: UserPreferencesRepository
#Before
#ExperimentalCoroutinesApi
fun init() {
hiltRule.inject()
// Execute all pending coroutine actions in MainViewModel initialization
mainCoroutineRule.runBlockingTest {
mainViewModel = MainViewModel(userPreferencesRepository)
}
}
#ExperimentalCoroutinesApi
#Test
fun `#setFirstTime marks the user as have opened the app at least once`() {
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(true))
mainCoroutineRule.runBlockingTest {
mainViewModel.setFirstTime()
}
# Failing assertion. Comes back as `true` when I expect it to be `false`
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(false))
}
}
MainViewModel.kt
package com.example.fitness.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.example.fitness.data.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
#HiltViewModel
class MainViewModel #Inject constructor(
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
val state = userPreferencesRepository.userPreferencesFlow.asLiveData()
/**
* Persists a value signifying that the user has started the app before.
*/
fun setFirstTime() {
viewModelScope.launch {
userPreferencesRepository.updateFirstTime(false)
}
}
}
UserPreferencesRepository
package com.example.fitness.data
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
data class UserPreferences(
val firstTime: Boolean
)
class UserPreferencesRepository #Inject constructor(private val dataStore: DataStore<Preferences>) {
private object PreferencesKeys {
val FIRST_TIME = booleanPreferencesKey("first_time")
}
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data.map { preferences ->
val firstTime = preferences[PreferencesKeys.FIRST_TIME] ?: true
UserPreferences(firstTime)
}
suspend fun updateFirstTime(firstTime: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.FIRST_TIME] = firstTime
}
}
}
I verified via the debugger that the body of the dataStore.edit code is being run prior to the last assertion of the test. I also noticed that the body of dataStore.data.map is also being run after the update, with the correctly populated preferences set to false. It appears that running the test in debug mode and quickly stepping through my break points results in a passing test, but running the test normally produces a failure, which leads me to believe there is some race condition present.
I am basing my work off of a Google Codelab. Any help would be greatly appreciated.
I managed to determine what the issue was. When I am creating my DataStore in the app, I am using the default coroutine scope, which is Dispatchers.IO. In my tests, I was replacing the main coroutine with kotlinx.coroutines.test.TestCoroutineDispatcher, but I needed to somehow instantiate the DataStore with a TestCoroutineScope as well, so that those saving actions would run synchronously.
Taking a lot of liberties from this extremely helpful article, my final code looks like:
MainViewModelTest.kt
#RunWith(AndroidJUnit4::class)
class MainViewModelTest : DataStoreTest() {
private lateinit var mainViewModel: MainViewModel
#Before
fun init() = runBlockingTest {
val userPreferencesRepository = UserPreferencesRepository(dataStore)
mainViewModel = MainViewModel(userPreferencesRepository)
}
#Test
fun `#setFirstTime marks the user as having opened the app at least once`() = runBlockingTest {
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(true))
mainViewModel.setFirstTime()
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(false))
}
}
DataStoreTest.kt
abstract class DataStoreTest : CoroutineTest() {
private lateinit var preferencesScope: CoroutineScope
protected lateinit var dataStore: DataStore<Preferences>
#Before
fun createDatastore() {
preferencesScope = CoroutineScope(testDispatcher + Job())
dataStore = PreferenceDataStoreFactory.create(scope = preferencesScope) {
InstrumentationRegistry.getInstrumentation().targetContext.preferencesDataStoreFile(
"test-preferences-file"
)
}
}
#After
fun removeDatastore() {
File(
ApplicationProvider.getApplicationContext<Context>().filesDir,
"datastore"
).deleteRecursively()
preferencesScope.cancel()
}
}
CoroutineTest.kt
abstract class CoroutineTest {
#Rule
#JvmField
val rule = InstantTaskExecutorRule()
protected val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testDispatcher)
#Before
fun setupViewModelScope() {
Dispatchers.setMain(testDispatcher)
}
#After
fun cleanupViewModelScope() {
Dispatchers.resetMain()
}
#After
fun cleanupCoroutines() {
testDispatcher.cleanupTestCoroutines()
testDispatcher.resumeDispatcher()
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest(block)
}

Some Junit5 test codes are not run

I am developing an android with Junit5 and Mockito.
Some tests are ParameterizedTest and others are just Test.
Here is my sample code.
When I run this test, only "ParameterizedTests" run.
"JustTests" is not shown on the JUnit test console list.
How can I run "JustTests" too?
import org.junit.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import org.mockito.InOrder
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.*
import org.mockito.MockitoAnnotations
internal class MyPresenterTest {
#Mock
private lateinit var view: MyContract.View
private lateinit var presenter: MyContract.Presenter
#BeforeEach
fun setup() {
MockitoAnnotations.openMocks(this)
presenter = MyPresenter(view)
}
#Nested
#DisplayName("Just Test")
inner class JustTests {
#DisplayName("test 1")
#Test
fun greetingTest1() {
...
}
}
#Nested
#DisplayName("Parameterized test")
inner class ParameterizedTests {
#ParameterizedTest(name = "{0}")
#ValueSource(strings = ["Hello", "Hi])
#Test
fun greetingTest2(greeting: String) {
...
}
}
}

Categories

Resources