I am trying to unit test my repository . In that i want to write test for function which makes api calls.(function name :- searchImage)
The function calls datasource for actual api calls.
I am using a fake data source which extends from the shopping data source interface.
How can i provide different responses from fake data source for the api calls like no internet , Null body or Success response.
ShoppingRepositoryInterface
interface ShoppingRepository {
suspend fun insertShoppingItem(shoppingItem: ShoppingItem)
suspend fun deleteShoppingItem(shoppingItem: ShoppingItem)
fun getAllShoppingItems() : LiveData<List<ShoppingItem>>
fun getTotalPrice() : LiveData<Float>
suspend fun searchImage(q:String) : Resource<ImageResponse>
}
ShoppingRepositoryImpl
#ViewModelScoped
class ShoppingRepositoryImpl #Inject constructor(
private val dataSource: ShoppingDataSource
) : ShoppingRepository{
override suspend fun insertShoppingItem(shoppingItem: ShoppingItem) = dataSource.insertShoppingItem(shoppingItem)
override suspend fun deleteShoppingItem(shoppingItem: ShoppingItem) = dataSource.deleteShoppingItem(shoppingItem)
override fun getAllShoppingItems() = dataSource.getAllShoppingItems()
override fun getTotalPrice() = dataSource.getTotalPrice()
override suspend fun searchImage(q: String) : Resource<ImageResponse> {
return try {
val response = dataSource.searchImage(q)
if(response.isSuccessful){
response.body()?.let {
Resource.Success(it)
}?:Resource.Error("Some Error Occurred")
}else{
Resource.Error("Some Error Occurred")
}
}catch (e:Exception){
Resource.Error("Couldn't connect to the server.Please try later")
}
}
}
ShoppingReposositoryTest
class ShoppingRepositoryTest{
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var repository: ShoppingRepository
private lateinit var dataSource: ShoppingDataSource
private val shoppingItem1 = ShoppingItem("name1",1,price=1f,"url1",1)
private val shoppingItem2 = ShoppingItem("name2",2,price=2f,"url2",2)
private val shoppingItem3 = ShoppingItem("name3",3,price=3f,"url3",3)
#Before
fun setup(){
dataSource = FakeDataSource(mutableListOf(
shoppingItem1
))
repository = ShoppingRepositoryImpl(dataSource)
}
#Test
#ExperimentalCoroutinesApi
fun insertShoppingItem_returnsWhetherItemInserted() = runTest{
//Given
//When
repository.insertShoppingItem(shoppingItem2)
val shoppingList = repository.getAllShoppingItems().getOrAwaitValue()
//Then
assertThat(shoppingList).contains(shoppingItem1)
}
#Test
#ExperimentalCoroutinesApi
fun deleteShoppingItem_returnsWhetherItemDeleted() = runTest{
//Given
//When
repository.deleteShoppingItem(shoppingItem1)
val shoppingList = repository.getAllShoppingItems().getOrAwaitValue()
//Then
assertThat(shoppingList.contains(shoppingItem1)).isFalse()
}
#Test
#ExperimentalCoroutinesApi
fun observeTotalPrice_returnsTotalPrice() = runTest {
//Given
//When
repository.insertShoppingItem(shoppingItem2)
repository.insertShoppingItem(shoppingItem3)
val actual = repository.getTotalPrice().getOrAwaitValue()
val expected = 14f
//Then
assertThat(expected).isEqualTo(actual)
}
#Test
#ExperimentalCoroutinesApi
fun observesSearchImage_returnsNoInternetError() = runTest {
//Given
//When
}
ShoppingDataSourceInterface
interface ShoppingDataSource {
suspend fun insertShoppingItem(shoppingItem:ShoppingItem)
suspend fun deleteShoppingItem(shoppingItem: ShoppingItem)
fun getAllShoppingItems() : LiveData<List<ShoppingItem>>
fun getTotalPrice() : LiveData<Float>
suspend fun searchImage(q:String) : Response<ImageResponse>
}
FakeDataDource
class FakeDataSource(private val list: MutableList<ShoppingItem> = mutableListOf()) : ShoppingDataSource {
private val shoppingList = MutableLiveData<List<ShoppingItem>>()
get() {
field.value=list
return field
}
private val totalPrice = MutableLiveData<Float>()
get(){
var value=0f
for(item in shoppingList.value!!){
value+=(item.price*item.amount)
}
field.value=value
return field
}
override suspend fun insertShoppingItem(shoppingItem: ShoppingItem) {
list.add(shoppingItem)
}
override suspend fun deleteShoppingItem(shoppingItem: ShoppingItem) {
list.remove(shoppingItem)
}
override fun getAllShoppingItems(): LiveData<List<ShoppingItem>> = shoppingList
override fun getTotalPrice(): LiveData<Float> {
return totalPrice
}
override suspend fun searchImage(q: String): Response<ImageResponse> {
TODO("")
}
I cannot use some booleans etc since it extends ShoppingDataSource Interface .
I tried mocking as well but that didn't work . What should be done now?
Related
im really new on AAC and repository.
I have made an app with MVVM and repository.
Activity
class UserTestActivity : AppCompatActivity() {
private val userViewModel : UserViewModel by viewModel<UserViewModel>()
private lateinit var button : AppCompatButton
private var count : Int =0
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_test
button = findViewById(R.id.testButton)
val userObserver = Observer<MutableList<UserModel>> { newUserList ->
Log.d("room-db-status", "size: "+newUserList.size)
}
userViewModel._user.observe(this, userObserver)
button.setOnClickListener(View.OnClickListener {
count++
Toast.makeText(this, "updated: "+count, Toast.LENGTH_SHORT).show()
userViewModel.insertUser(UserModel(
uid = count.toString(),
nickName = "Alexar",
gender ="female",
age = 22,
birth ="19990901",
mainResidence= "Seoul",
subResidence = "???",
tripWish = mutableListOf("!!!","!!?"),
tripStyle = mutableListOf("!!!","!!?"),
selfIntroduction = "hi -_-",
uriList = mutableListOf("!!!","!!?"),
geohash = "none",
latitude = 37.455,
longitude = 124.890,
mannerScore = 4.5,
premiumOrNot = false,
knock = 0
))
})
}
}
this is ViewModel
class UserViewModel (
private val userRepository : UserRepository): ViewModel() {
val _user : LiveData<MutableList<UserModel>> = liveData(Dispatchers.IO) {
val data = userRepository.getAllUser()
emit(data)
}
fun insertUser (userModel: UserModel) {
viewModelScope.launch(Dispatchers.IO) {
userRepository.insertUser(userModel)
}
}
}
Repositoty
interface UserRepository {
suspend fun getAllUser () : MutableList<UserModel>
suspend fun insertUser (userModel: UserModel)
}
RepositoryImpl
class UserRepositoryImpl (
private val localDataSource : UserLocalDataSource,
private val userMapper: UserMapper) :UserRepository{
override suspend fun getAllUser() : MutableList<UserModel> {
val userList : List<UserEntity> = localDataSource.getAllUser()
var temp = mutableListOf<UserModel>()
for (user in userList)
temp.add(userMapper.entityToModel(user))
return temp
}
override suspend fun insertUser(userModel: UserModel) {
return localDataSource.insertUser(userMapper.modelToEntity(userModel))
}
}
UserLocalDataSource
interface UserLocalDataSource {
suspend fun getAllUser () : MutableList<UserEntity>
suspend fun insertUser (userEntity: UserEntity)
}
UserLocalDataSourceImpl
class UserLocalDataSourceImpl(
private val appDatabase: AppDatabase) : UserLocalDataSource {
override suspend fun getAllUser() : MutableList<UserEntity> {
return appDatabase.userEntityDao().getAllUser()
}
override suspend fun insertUser(userEntity: UserEntity) {
appDatabase.userEntityDao().insertUser(userEntity)
}
}
UserEntityDao
interface UserEntityDAO {
#Query ("SELECT * FROM user " )
suspend fun getAllUser() : MutableList<UserEntity>
#Query ("SELECT * FROM user WHERE uid = (:uid) ")
suspend fun getUser(uid: String) :UserEntity
#Insert (onConflict = REPLACE)
suspend fun insertUser (user : UserEntity)
#Query("DELETE FROM user WHERE uid = (:uid)")
suspend fun delete(uid : String)
}
there are also Mapper and Koin injection.
when trying to insert user data to room, it was successful. but
after that, liveData Builder
val _user : LiveData<MutableList<UserModel>> = liveData(Dispatchers.IO) {
val data = userRepository.getAllUser()
emit(data)
}
not invoked...
Of course, that builder is invoked only once when app started haha
who knows why??
I do not know.
You shouldn't rely on the liveData builder to be invoked when data is inserted/updated in DB. Please refer to this doc on how to work with liveData builder.
To achieve the case when an observer of LiveData is invoked when data is inserted/updated in DB, methods must return LiveData object in UserEntityDao:
interface UserEntityDAO {
#Query ("SELECT * FROM user " )
suspend fun getAllUser() : LiveData<List<UserEntity>>
#Query ("SELECT * FROM user WHERE uid = (:uid) ")
suspend fun getUser(uid: String) : LiveData<UserEntity>
// ...
}
I am trying to test my app with firestore and I don't know how will I fake the datasource with a Flow to test my repository. I'm trying to follow the pattern from google codelabs but I only convert toObject in the composable. How can I write the repository test?
below is my FirestoreDatasource and my Repository and the FakeDatasource I am trying to write
#Singleton
class FirestoreDatasourceImpl #Inject constructor(
private val firestore: FirebaseFirestore
) : FirestoreDatasource {
override suspend fun addUser(user: UserModel) {
firestore.collection(USERS)
.document(user.id)
.set(user, SetOptions.merge())
}
#ExperimentalCoroutinesApi
override fun getAllUsers(userId: String) = callbackFlow {
val collection = firestore.collection(USERS).whereNotEqualTo("id", userId)
val snapshotListener = collection.addSnapshotListener() { snapshot, e ->
this.trySend(snapshot).isSuccess
}
awaitClose {
snapshotListener.remove()
}
}
}
#Singleton
class FirestoreRepositoryImpl #Inject constructor(
private val firestoreClass: FirestoreDatasourceImpl
) : FirestoreRepository{
override suspend fun addUser(user: UserModel){
firestoreClass.addUser(user)
}
#ExperimentalCoroutinesApi
override fun getAllUsers(userId: String) : Flow<QuerySnapshot?> {
return firestoreClass.getAllUsers(userId)
}
}
internal class FakeFirestoreDatasourceImplTest constructor(
private var userList: MutableList<UserModel>? = mutableListOf()
) : FirestoreDatasource{
override suspend fun addUser(user: UserModel) {
userList?.add(user)
}
override fun getAllUsers(userId: String): Flow<QuerySnapshot?> {
}
}
I am not sure what is exactly happening but when ApiService.apiService.getPokemon(name) in fun getPokemon in PokemonRepository.kt is called then the function getPokemon stops executing and emited livedata are then observed as null in DetailAktivity.kt instead of a valid Pokemon class.
I have checked the API call and it is working in other cases. I am new to Android programming, so I would appreciate some detailed explanation.
Here are the classes:
PokemonRepository.kt
class PokemonRepository(context: Context) {
companion object {
private val TAG = PokemonRepository::class.java.simpleName
}
private val pekemonDao = PokemonDatabase.getInstance(context).pokemonDao()
fun getPokemon(name: String) = liveData {
val disposable = emitSource(
pekemonDao.getOne(name).map {
it
}
)
val pokemon = ApiService.apiService.getPokemon(name)
try {
disposable.dispose()
pekemonDao.insertAllPokemons(pokemon)
pekemonDao.getOne(name).map {
it
}
} catch (e: Exception) {
Log.e(TAG, "Getting data from the Internet failed", e)
pekemonDao.getOne(name).map {
e
}
}
}
DetailActivity.kt
class DetailActivity : AppCompatActivity() {
companion object {
const val ITEM = "item"
}
private lateinit var binding: ActivityDetailBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
val vm: DetailViewModel by viewModels()
vm.pokemon.observe(
this,
{
binding.name.text = it.name
supportActionBar?.apply {
setDisplayShowTitleEnabled(true)
title = it.name
}
}
)
intent.extras?.apply {
vm.setCharacterId(getString(ITEM)!!)
}
}
}
DetailViewModel
class DetailViewModel(application: Application) : AndroidViewModel(application) {
private val repository = PokemonRepository(application)
private val name: MutableLiveData<String> = MutableLiveData()
val pokemon = name.switchMap { name ->
repository.getPokemon(name)
}
fun setCharacterId(characterId: String) {
name.value = characterId
}
}
ApiService.kt
interface ApiService {
#GET("pokemon?offset=0&limit=151")
suspend fun getPokemons(#Query("page") page: Int): NamedApiResourceList
#GET("pokemon/{name}")
suspend fun getPokemon(#Path("name") name: String): Pokemon
companion object {
private const val API_ENDPOINT = "https://pokeapi.co/api/v2/"
val apiService by lazy { create() }
private fun create(): ApiService = Retrofit.Builder()
.baseUrl(API_ENDPOINT)
.addConverterFactory(MoshiConverterFactory.create())
.client(OkHttpClient())
.build()
.create(ApiService::class.java)
}
}
Pokemon data class
#Parcelize
#JsonClass(generateAdapter = true)
#Entity
data class Pokemon(
#PrimaryKey val id: Int,
val name: String,
#ColumnInfo(name = "base_experience") val baseExperience: Int,
val height: Int,
#ColumnInfo(name = "is_default") val isDefault: Boolean,
val order: Int,
val weight: Int,
val sprites: PokemonSprites,
) : Parcelable
PokemonDao.kt
#Dao
interface PokemonDao {
#Query("SELECT * FROM namedapiresource")
fun getAll(): LiveData<List<NamedApiResource>>
#Query("SELECT * FROM pokemon WHERE name=:name")
fun getOne(name: String): LiveData<Pokemon>
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAllNamedApiResources(vararg characters: NamedApiResource)
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAllPokemons(vararg characters: Pokemon)
}
I would guess because of how your getPokemon is defined:
val disposable = emitSource(
// Attempts to get a pokemon from the database - presumably this does
// not exist at first so would return null first
pekemonDao.getOne(name).map {
it
}
)
// AFTER NULL IS RETURNED this tries to fetch from the API
val pokemon = ApiService.apiService.getPokemon(name)
try {
disposable.dispose()
pekemonDao.insertAllPokemons(pokemon)
// After fetching from API, finally returns a non-null
pekemonDao.getOne(name).map {
it
}
So maybe just get ride of the initial block?
val disposable = emitSource(
pekemonDao.getOne(name).map {
it
}
)
I want call hight order suspend function from other class with parameter and i don't know how.
class CharactersListViewModel : ViewModel() {
private val dataSourceFactory =
PageKeyDataSourceFactory(
scope = viewModelScope,
request = suspend {createRequest(0)
}
)
private suspend inline fun createRequest(offset : Int): MutableList<CharacterItem> {
val repository = Injection.provideMarvelRepository()
val response = repository.getCharacters(
offset = offset,
limit = PAGE_MAX_ELEMENTS
)
return CharacterItemMapper().map(response).toMutableList()
}
other class
class PageKeyDataSourceFactory<Value>(
private val scope: CoroutineScope,
private var request: suspend () -> MutableList<Value>
) : DataSource.Factory<Int, Value>() {
private var dataSource: PageKeyDataSource<Value>? = null
override fun create(): DataSource<Int, Value> {
dataSource = PageKeyDataSource(request = request, scope)
sourceLiveData.postValue(dataSource)
return dataSource as PageKeyDataSource<Value>
}
and class here i call function
in loadAfter function comes a params that I want to be used to call request.invoke()
class PageKeyDataSource<Value>(
private val request: suspend() -> MutableList<Value>,
private val scope: CoroutineScope,
) : PageKeyedDataSource<Int, Value>() {
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Value>) {
scope.launch(
CoroutineExceptionHandler { _, _ ->
retry = {
loadAfter(params, callback)
}
networkState.postValue(NetworkState.Error(true))
}
) {
val data = request.invoke()
callback.onResult(data, params.key + PAGE_MAX_ELEMENTS)
networkState.postValue(NetworkState.Success(true, data.isEmpty()))
}
}
}
class PageKeyDataSource<Value>(private val request: suspend (yourParams:YourType) -> MutableList<Value>) {
...
// some code
val list:MutableList<Value> = request.invoke(yourParams)
}
i am implementing Room with a vIewModel, my structure is the following
#DAO,#Entity,#Database,#Repository
#Entity(tableName="dx_table")
class dx_table(
#ColumnInfo(name = "name")
val naxme: String,
#PrimaryKey
#ColumnInfo(name = "phone")
val phone: String,
#ColumnInfo(name = "passx")
val passx: String,
#ColumnInfo(name = "login_fx")
val login_fx: String
)
#Dao
interface dx_dao{
#Query("SELECT * FROM dx_table")
fun get_all():LiveData<List<dx_table>>
#Insert
suspend fun insertTrx(dx_table:dx_table)
#Query("UPDATE dx_table SET login_fx =:login_fx where phone=:phonex")
suspend fun insertFx(login_fx: String,phonex: String)
#Query("SELECT * from dx_table where phone=:phonex")
suspend fun get_name_px(phonex: String):List<dx_table>
#Query("Delete from dx_table")
suspend fun deleteAll()
#Query("Select * from dx_table where login_fx=1")
suspend fun selectFx():List<dx_table>
}
#Database(entities = arrayOf(dx_table::class), version = 1, exportSchema = false)
public abstract class DxDatabase : RoomDatabase() {
abstract fun dxDao(): dx_dao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
#Volatile
private var INSTANCE: DxDatabase? = null
fun getDatabase(context: Context): DxDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
DxDatabase::class.java,
"dx_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
class dxRepository(private val dxDao: dx_dao ){
val k_d:LiveData<List<dx_table>> = dxDao.get_all()
suspend fun insert_trx(dx_table: dx_table){
dxDao.insertTrx(dx_table)
}
suspend fun insert_fx(login_fx: String,phonex: String) {
dxDao.insertFx(login_fx,phonex)
}
suspend fun select_fx() {
dxDao.selectFx()
}
suspend fun get_name_px(phonex: String) :List<dx_table> {
return dxDao.get_name_px(phonex) as List<dx_table>
}
}
The viewmodel is
class DxViewModel (application: Application) : AndroidViewModel(application) {
var repository: dxRepository
var k_d: LiveData<List<dx_table>>
init {
// Gets reference to WordDao from WordRoomDatabase to construct
// the correct WordRepository.
val dxDao = DxDatabase.getDatabase(application).dxDao()
repository = dxRepository(dxDao)
k_d = repository.k_d
}
fun insert_trx(dxTable: dx_table) = viewModelScope.launch {
repository.insert_trx(dxTable)
}
fun insert_fx(login_fx: String, phonex: String) = viewModelScope.launch {
repository.insert_fx(login_fx, phonex)
}
fun select_fx() = viewModelScope.launch {
repository.select_fx()
}
fun get_name_px(phonex: String) = viewModelScope.launch {
repository.get_name_px(phonex)
}
}
I can track the live data using observe,its not an issue, the problem i am facing is with the get_name_px(phone)
var mView = ViewModelProviders.of(this).get(DxViewModel::class.java)
var lm = mView.get_name_px(phone)
here lm seems to be job type , i need the return List , how do i get it .
In your viewModel function select_fx() return a job, because launch does not return result, so you have to either:
1) Use async and await
fun get_name_px(phonex: String) = viewModelScope.async {
repository.get_name_px(phonex)
}.await()
2) Not use launch viewModel, use it in Activity/Fragment
suspend fun get_name_px(phonex: String) = repository.get_name_px(phonex)
class CardFragment : Fragment() {
fun get() {
// launch in Dispatchers.Main
lifecycleScope.launch {
var lm = mView.get_name_px(phone)
}
// launch in background thread
lifecycleScope.launch(Dispatchers.Default) {
var lm = mView.get_name_px(phone)
}
}
}