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)
}
}
Related
I'm trying to follow MVVM pattern to fetch data from the given api but getting error while Initiating a connection. My application gets crashed showing the error in the logcat.
My ModalClass.kt
package com.example.retrofitdemo2.api
class ModelClass : ArrayList<ModelClassItem>()
ModelClassItem:
package com.example.retrofitdemo2.api
data class ModelClassItem(
val body: String,
val id: Int,
val title: String,
val userId: Int
)
RetrofitHelperClass.kt
package com.example.retrofitdemo2.api
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class RetrofitHelperClass {
companion object {
private val BASE_URL = "https://jsonplaceholder.typicode.com/"
var interceptor = HttpLoggingInterceptor().apply {
this.level = HttpLoggingInterceptor.Level.BODY
}
var client = OkHttpClient.Builder().apply {
this.addInterceptor(interceptor)
}.build()
fun getInstance(): Retrofit {
return Retrofit.Builder().baseUrl(BASE_URL).client(client)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())).build()
}
}
}
RetrofitService.kt
package com.example.retrofitdemo2.api
import androidx.lifecycle.LiveData
import retrofit2.Response
import retrofit2.http.GET
interface RetrofitService {
#GET("/albums")
fun get(): LiveData<Response<ModelClass>>
}
Repository.kt: Here i'm getting errors which is mentioned at the end.
Seems like repository is unable to create Adapter call.enter code here
package com.example.retrofitdemo2.repository
import com.example.retrofitdemo2.api.RetrofitService
class Repositroy(retrofitService: RetrofitService) {
val response = retrofitService.get()
}
viewmodel.kt:
package com.example.retrofitdemo2.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.retrofitdemo2.api.ModelClass
import com.example.retrofitdemo2.api.ModelClassItem
import com.example.retrofitdemo2.repository.Repositroy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainViewModel(repositroy: Repositroy) : ViewModel() {
var getdata = repositroy.response
}
MainViewModelFactory.kt:
package com.example.retrofitdemo2.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.retrofitdemo2.repository.Repositroy
class MainViewModelFactory(private val repositroy: Repositroy) :
ViewModelProvider.Factory
{
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)){
return MainViewModel(repositroy) as T
}
throw IllegalArgumentException("Problem in View Model Factory")
}
}
MainActivity.kt:
package com.example.retrofitdemo2
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.example.retrofitdemo2.api.RetrofitHelperClass
import com.example.retrofitdemo2.api.RetrofitService
import com.example.retrofitdemo2.databinding.ActivityMainBinding
import com.example.retrofitdemo2.repository.Repositroy
import com.example.retrofitdemo2.viewmodel.MainViewModel
import com.example.retrofitdemo2.viewmodel.MainViewModelFactory
import retrofit2.Retrofit
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewmodel : MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
var retorservice =
RetrofitHelperClass.getInstance().create(RetrofitService::class.java)
val repositroy = Repositroy(retorservice)
val factory = MainViewModelFactory(repositroy)
viewmodel = ViewModelProvider(this, factory).get(MainViewModel::class.java)
binding.viewmodel = viewmodel
binding.lifecycleOwner = this
viewmodel.getdata.observe(this, Observer {
Log.i("MainActivity", "${it}")
})
}
}
Error:
Unable to start activity
ComponentInfo{com.example.retrofitdemo2/com.example.retrofitdemo2.MainActivity}:
java.lang.IllegalArgumentException: Unable to create call adapter for
androidx.lifecycle.LiveData<retrofit2.Response<com.example.retrof
at com.example.retrofitdemo2.repository.Repositroy.<init>(Repositroy.kt:7)
at com.example.retrofitdemo2.MainActivity.onCreate(MainActivity.kt:28)
at android.app.Activity.performCreate(Activity.java:8109)
at android.app.Activity.performCreate(Activity.java:8083)
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.
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)
}
I am trying to use my hilt viewModel in a composable function but I keep getting the error:
"java.lang.RuntimeException: Cannot create an instance of class com.example.tryingtogettheviewmodeltowork.MainViewModel"
I am using simple hilt injection in my viewModel to replicate the error that I am getting on the app I am actually working on. Android documentation says that nothing else needs to be done in the composable function to use a hilt viewModel (https://developer.android.com/jetpack/compose/libraries#hilt)however I keep getting the same error every time I try another solution.
My viewModel class:
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
#HiltViewModel
class MainViewModel #Inject constructor(
val someInt: Int
): ViewModel(){
init{
println(someInt)
}
// Mutable Live data for storing the value of the username entry field
private var _username = MutableLiveData("")
val username: LiveData<String> =_username
// Function for changing the value of the username entry field
fun onUsernameChange(it: String){
_username.value = it
}
}
My MainActivity Class with composable function:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import com.example.tryingtogettheviewmodeltowork.ui.theme.TryingToGetTheViewModelToWorkTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TryingToGetTheViewModelToWorkTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
MainScreen()
}
}
}
}
}
#Composable
fun MainScreen(viewModel: MainViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
val userName: String by viewModel.username.observeAsState("")
OutlinedTextField(
value = userName,
onValueChange ={
viewModel.onUsernameChange(it)
},
label = {
Text(text = "User Name")
}
)
}
}
I figured out my mistake. You have to add #AndroidEntryPoint to MainActiivty rather than trying to add it on top of a composable function.
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) {
...
}
}
}