Updates to unit tests

Added more units tests - room database test
Amended ui
Changed how recycler view loads data
This commit is contained in:
2020-11-20 15:34:17 +00:00
parent 28fa9fd258
commit 71968d39b8
27 changed files with 342 additions and 236 deletions

View File

@@ -70,6 +70,11 @@ dependencies {
implementation 'android.arch.core:core-testing'
androidTestImplementation 'androidx.test:rules:1.3.0-rc01'
// Mockk
def mockk_ver = "1.10.2"
testImplementation "io.mockk:mockk:$mockk_ver"
androidTestImplementation "io.mockk:mockk-android:$mockk_ver"
//Retrofit and GSON
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'
@@ -96,4 +101,6 @@ dependencies {
// Circle Image View
implementation 'com.mikhaellopez:circularimageview:4.2.0'
// Latest version of JSONObject - needed to pass unit tests
testImplementation 'org.json:json:20180813'
}

View File

@@ -1,22 +0,0 @@
package com.example.h_mal.stackexchangeusers
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.h_mal.candyspace", appContext.packageName)
}
}

View File

@@ -0,0 +1,86 @@
package com.example.h_mal.stackexchangeusers.data.room
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.matcher.ViewMatchers
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
class AppDatabaseTest{
@get:Rule
var rule: TestRule = InstantTaskExecutorRule()
private lateinit var simpleDao: UsersDao
private lateinit var db: AppDatabase
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context, AppDatabase::class.java)
.build()
simpleDao = db.getUsersDao()
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
@Test
@Throws(Exception::class)
fun writeEntryAndReadResponse(){
// Given
val entity = UserEntity(123)
val latch = CountDownLatch(1)
var userEntity: UserEntity? = null
// When
simpleDao.saveAllUsers(listOf(entity))
// Then
simpleDao.getUser(123).observeForever {
userEntity = it
latch.countDown()
}
ViewMatchers.assertThat(userEntity, CoreMatchers.equalTo(entity))
}
@Test
@Throws(Exception::class)
fun upsertUsersAndReadResponse() = runBlocking{
// Given
val userEntity = UserEntity(123)
val newUserEntity = UserEntity(456)
var result: List<UserEntity>? = null
// When
simpleDao.saveAllUsers(listOf(userEntity))
simpleDao.upsertNewUsers(listOf(newUserEntity))
// Then
val latch = CountDownLatch(1)
simpleDao.getAllUsers().observeForever {
result = it
latch.countDown()
}
ViewMatchers.assertThat(newUserEntity, CoreMatchers.equalTo(result?.get(0)))
}
}

View File

@@ -0,0 +1,27 @@
package com.example.h_mal.stackexchangeusers.data.model
import androidx.room.PrimaryKey
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
data class UserItem(
val userId: Int?,
val displayName: String?,
val bronze: Int?,
val silver: Int?,
val gold: Int?,
val reputation: Int?,
val creationDate: Int?,
val profileImage: String?
){
constructor(userEntity: UserEntity): this(
userEntity.userId,
userEntity.displayName,
userEntity.bronze,
userEntity.silver,
userEntity.gold,
userEntity.reputation,
userEntity.creationDate,
userEntity.profileImage
)
}

View File

@@ -2,7 +2,7 @@ package com.example.h_mal.stackexchangeusers.data.network.api
import com.example.h_mal.stackexchangeusers.data.network.api.interceptors.NetworkConnectionInterceptor
import com.example.h_mal.stackexchangeusers.data.network.api.interceptors.QueryParamsInterceptor
import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse
import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse
import okhttp3.OkHttpClient
import retrofit2.Response
import retrofit2.Retrofit

View File

@@ -5,7 +5,7 @@ import org.json.JSONObject
import retrofit2.Response
import java.io.IOException
abstract class ResponseUnwrap {
abstract class SafeApiCall {
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun <T : Any> responseUnwrap(

View File

@@ -1,4 +1,4 @@
package com.example.h_mal.stackexchangeusers.data.network.model
package com.example.h_mal.stackexchangeusers.data.network.response
data class ApiResponse(
val items : List<User>?,

View File

@@ -1,4 +1,4 @@
package com.example.h_mal.stackexchangeusers.data.network.model
package com.example.h_mal.stackexchangeusers.data.network.response
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName

View File

@@ -1,4 +1,4 @@
package com.example.h_mal.stackexchangeusers.data.network.model
package com.example.h_mal.stackexchangeusers.data.network.response
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName

View File

@@ -1,14 +1,14 @@
package com.example.h_mal.stackexchangeusers.data.repositories
import androidx.lifecycle.LiveData
import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse
import com.example.h_mal.stackexchangeusers.data.network.model.User
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse
import com.example.h_mal.stackexchangeusers.data.network.response.User
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
interface Repository {
fun getUsersFromDatabase(): LiveData<List<UserItem>>
fun getSingleUserFromDatabase(id: Int): LiveData<UserItem>
fun getUsersFromDatabase(): LiveData<List<UserEntity>>
fun getSingleUserFromDatabase(id: Int): LiveData<UserEntity>
suspend fun saveUsersToDatabase(users: List<User>)
suspend fun getUsersFromApi(username: String): ApiResponse?
fun saveCurrentSearchToPrefs(username: String)

View File

@@ -1,33 +1,29 @@
package com.example.h_mal.stackexchangeusers.data.repositories
import com.example.h_mal.stackexchangeusers.data.network.api.ApiClass
import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse
import com.example.h_mal.stackexchangeusers.data.network.model.User
import com.example.h_mal.stackexchangeusers.data.network.networkUtils.ResponseUnwrap
import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse
import com.example.h_mal.stackexchangeusers.data.network.response.User
import com.example.h_mal.stackexchangeusers.data.network.networkUtils.SafeApiCall
import com.example.h_mal.stackexchangeusers.data.preferences.PreferenceProvider
import com.example.h_mal.stackexchangeusers.data.room.AppDatabase
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
const val MILLISECONDS_ONE_MIN = 60000
class RepositoryImpl(
private val api: ApiClass,
private val database: AppDatabase,
private val preference: PreferenceProvider
) : ResponseUnwrap(), Repository {
) : SafeApiCall(), Repository {
// Current list of users in the database
override fun getUsersFromDatabase() = database.getUsersDao().getAllUsers()
// retrieving a single user from an ID
override fun getSingleUserFromDatabase(id: Int) = database.getUsersDao().getUser(id)
// save a list of users to the room database
override suspend fun saveUsersToDatabase(users: List<User>){
val userList= getUserList(users)
val userList= users.map { UserEntity(it) }
database.getUsersDao().upsertNewUsers(userList)
}
// fetch users from an api call
override suspend fun getUsersFromApi(username: String): ApiResponse? {
return when (isSearchValid(username)) {
true -> responseUnwrap { api.getUsersFromApi(username) }
@@ -35,14 +31,12 @@ class RepositoryImpl(
}
}
// save current time and current search input into shared prefs
override fun saveCurrentSearchToPrefs(username: String){
val time = System.currentTimeMillis()
preference.saveLastSavedAt(username, time)
}
// boolean response of validity of search
// if the same search is taking place again with a minute return false
// Check if search is valid based on lasted saved
private fun isSearchValid(username: String): Boolean {
val time = preference.getLastSavedAt(username) ?: return true
val currentTime = System.currentTimeMillis()
@@ -50,24 +44,4 @@ class RepositoryImpl(
return difference > MILLISECONDS_ONE_MIN
}
// map user from api response to userItem entity for database
private fun getUserList(users: List<User>): List<UserItem> {
return users.map {
it.mapToUserItem()
}
}
private fun User.mapToUserItem(): UserItem {
return UserItem(
userId,
displayName,
badgeCounts?.bronze,
badgeCounts?.silver,
badgeCounts?.gold,
reputation,
creationDate,
profileImage
)
}
}

View File

@@ -4,10 +4,10 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
@Database(
entities = [UserItem::class],
entities = [UserEntity::class],
version = 1
)
abstract class AppDatabase : RoomDatabase() {

View File

@@ -2,27 +2,27 @@ package com.example.h_mal.stackexchangeusers.data.room
import androidx.lifecycle.LiveData
import androidx.room.*
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
@Dao
interface UsersDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun saveAllUsers(users : List<UserItem>)
fun saveAllUsers(users : List<UserEntity>)
@Query("SELECT * FROM UserItem")
fun getAllUsers() : LiveData<List<UserItem>>
@Query("SELECT * FROM UserEntity")
fun getAllUsers() : LiveData<List<UserEntity>>
// clear database and add new entries
@Transaction
suspend fun upsertNewUsers(users : List<UserItem>){
suspend fun upsertNewUsers(users : List<UserEntity>){
deleteEntries()
saveAllUsers(users)
}
@Query("DELETE FROM UserItem")
@Query("DELETE FROM UserEntity")
suspend fun deleteEntries()
@Query("SELECT * FROM UserItem WHERE userId = :id")
fun getUser(id: Int) : LiveData<UserItem>
@Query("SELECT * FROM UserEntity WHERE userId = :id")
fun getUser(id: Int) : LiveData<UserEntity>
}

View File

@@ -0,0 +1,30 @@
package com.example.h_mal.stackexchangeusers.data.room.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.example.h_mal.stackexchangeusers.data.network.response.User
@Entity
data class UserEntity(
@PrimaryKey(autoGenerate = false)
val userId: Int,
val displayName: String? = null,
val bronze: Int? = null,
val silver: Int? = null,
val gold: Int? = null,
val reputation: Int? = null,
val creationDate: Int? = null,
val profileImage: String? = null
){
constructor(user: User): this(
user.userId!!,
user.displayName,
user.badgeCounts?.bronze,
user.badgeCounts?.silver,
user.badgeCounts?.gold,
user.reputation,
user.creationDate,
user.profileImage
)
}

View File

@@ -1,17 +0,0 @@
package com.example.h_mal.stackexchangeusers.data.room.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class UserItem(
@PrimaryKey(autoGenerate = false)
val userId: Int?,
val displayName: String?,
val bronze: Int?,
val silver: Int?,
val gold: Int?,
val reputation: Int?,
val creationDate: Int?,
val profileImage: String?
)

View File

@@ -1,9 +1,12 @@
package com.example.h_mal.stackexchangeusers.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import com.example.h_mal.stackexchangeusers.application.AppClass.Companion.idlingResources
import com.example.h_mal.stackexchangeusers.data.model.UserItem
import com.example.h_mal.stackexchangeusers.data.repositories.Repository
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
import com.example.h_mal.stackexchangeusers.utils.Event
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -16,11 +19,19 @@ class MainViewModel(
) : ViewModel() {
// livedata for user items in room database
val usersLiveData = repository.getUsersFromDatabase()
val usersLiveData = MutableLiveData<List<UserItem>>()
val operationState = MutableLiveData<Event<Boolean>>()
val operationError = MutableLiveData<Event<String>>()
init {
val observer = Observer<List<UserEntity>> {
val list = it.map {entity -> UserItem(entity) }
usersLiveData.postValue(list)
}
repository.getUsersFromDatabase().observeForever (observer)
}
fun getUsers(username: String?){
// validate that search term is not empty

View File

@@ -9,8 +9,8 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.h_mal.stackexchangeusers.R
import com.example.h_mal.stackexchangeusers.application.AppClass.Companion.idlingResources
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
import com.example.h_mal.stackexchangeusers.data.model.UserItem
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
import com.example.h_mal.stackexchangeusers.ui.MainViewModel
import com.example.h_mal.stackexchangeusers.utils.onSubmitListener
import kotlinx.android.synthetic.main.main_fragment.*
@@ -21,11 +21,12 @@ import kotlinx.android.synthetic.main.main_fragment.*
* Results for users will be displayed here
*/
class MainFragment : Fragment() {
/**
* get [MainViewModel] from [MainActivity] (the calling activity)
*/
// Retrieve view model from main activity
val viewModel: MainViewModel by activityViewModels()
lateinit var mAdapter: UserRecyclerViewAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@@ -42,6 +43,13 @@ class MainFragment : Fragment() {
viewModel.getUsers(query.toString().trim())
}
}
mAdapter = UserRecyclerViewAdapter(requireView())
recycler_view.apply {
layoutManager = LinearLayoutManager(context)
setHasFixedSize(true)
adapter = mAdapter
}
}
override fun onStart() {
@@ -55,18 +63,7 @@ class MainFragment : Fragment() {
}
private val usersObserver = Observer<List<UserItem>> {
// create adapter the recycler view
val mAdapter = UserRecyclerViewAdapter(
requireView(),
it
)
// setup the recyclerview
recycler_view.apply {
layoutManager = LinearLayoutManager(context)
setHasFixedSize(true)
adapter = mAdapter
}
mAdapter.updateList(it)
}
}

View File

@@ -3,24 +3,33 @@ package com.example.h_mal.stackexchangeusers.ui.main.pages.home
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.persistableBundleOf
import androidx.recyclerview.widget.RecyclerView
import com.example.h_mal.stackexchangeusers.R
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
import com.example.h_mal.stackexchangeusers.data.model.UserItem
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
import com.example.h_mal.stackexchangeusers.utils.loadImage
import com.example.h_mal.stackexchangeusers.utils.navigateTo
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.simple_item.view.*
/**
* Create a Recyclerview adapter used to display [UserItem]
* Create a Recyclerview adapter used to display [UserEntity]
* @param parentView is used for context and navigation
* @param items is the list of users parsed from the database
*/
class UserRecyclerViewAdapter (
val parentView: View,
val items: List<UserItem>
val parentView: View
): RecyclerView.Adapter<RecyclerView.ViewHolder>(){
var list = mutableListOf<UserItem>()
fun updateList(users: List<UserItem>) {
list.clear()
list.addAll(users)
notifyDataSetChanged()
}
// create a viewholder with [R.layout.simple_item]
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater
@@ -31,13 +40,13 @@ class UserRecyclerViewAdapter (
}
override fun getItemCount(): Int {
return items.size
return list.size
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val view = holder as ItemOne
items[position].let {
list[position].let {
view.bindUser(it)
}
}
@@ -53,8 +62,7 @@ class UserRecyclerViewAdapter (
itemView.text2.text = userItem.reputation.toString()
itemView.setOnClickListener {
val action =
MainFragmentDirections.toUserProfileFragment(items[position].userId!!)
val action = MainFragmentDirections.toUserProfileFragment(list[layoutPosition].userId!!)
parentView.navigateTo(action)
}
}

View File

@@ -8,16 +8,14 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import com.example.h_mal.stackexchangeusers.R
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
import com.example.h_mal.stackexchangeusers.ui.MainViewModel
import com.example.h_mal.stackexchangeusers.utils.epochToData
import com.example.h_mal.stackexchangeusers.utils.loadImage
import kotlinx.android.synthetic.main.fragment_user_profile.*
/**
* A simple [Fragment] subclass.
* Use the [UserProfileFragment.newInstance] factory method to
* create an instance of this fragment.
* Fragment to show the user overview
*/
class UserProfileFragment : Fragment() {
val viewModel: MainViewModel by activityViewModels()
@@ -43,7 +41,7 @@ class UserProfileFragment : Fragment() {
viewModel.getSingleUser(id).removeObserver(singleUserObserver)
}
private val singleUserObserver = Observer<UserItem> {
private val singleUserObserver = Observer<UserEntity> {
username.text = it.displayName
reputation.text = it.reputation.toString()
gold_score.text = it.gold.toString()

View File

@@ -7,7 +7,7 @@ import java.util.*
fun epochToData(number: Int?): String {
number ?: return ""
return try {
val sdf = SimpleDateFormat("dd/MM/yyyy")
val sdf = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault())
val netDate = Date(number.toLong() * 1000)
sdf.format(netDate)
} catch (e: Exception) {

View File

@@ -4,20 +4,21 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
tools:context=".ui.main.pages.user.UserProfileFragment">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintHeight_percent="0.25"
android:adjustViewBounds="true"
app:layout_constraintVertical_bias="0.12"
android:src="@mipmap/ic_launcher"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHeight_percent="0.25"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.12"
tools:src="@drawable/ic_launcher_foreground" />
<TextView
@@ -25,37 +26,38 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Username:"
android:text="@string/username"
app:layout_constraintRight_toLeftOf="@id/divider"
app:layout_constraintTop_toBottomOf="@id/imageView" />
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@{user.displayName}"
android:maxLines="1"
app:layout_constraintLeft_toRightOf="@id/divider"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView"
tools:text="Username1234" />
<TextView
android:id="@+id/reputation_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Reputation:"
android:text="@string/reputation"
app:layout_constraintRight_toLeftOf="@id/divider"
app:layout_constraintTop_toBottomOf="@id/username_label" />
<TextView
android:id="@+id/reputation"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@{user.reputation.toString()}"
android:maxLines="1"
app:layout_constraintLeft_toRightOf="@id/divider"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username"
tools:text="1548252" />
@@ -64,7 +66,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Bagdes:"
android:text="@string/bagdes"
app:layout_constraintRight_toLeftOf="@id/divider"
app:layout_constraintTop_toBottomOf="@id/reputation_label" />
@@ -73,16 +75,17 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Gold - "
android:text="@string/gold"
app:layout_constraintLeft_toRightOf="@id/divider"
app:layout_constraintTop_toBottomOf="@+id/reputation" />
<TextView
android:id="@+id/gold_score"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{user.badgeCounts.gold.toString()}"
android:maxLines="1"
app:layout_constraintLeft_toRightOf="@id/badges_gold"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/badges_gold"
tools:text="3554" />
@@ -91,16 +94,17 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="Silver - "
android:text="@string/silver"
app:layout_constraintLeft_toRightOf="@id/divider"
app:layout_constraintTop_toBottomOf="@+id/badges_gold" />
<TextView
android:id="@+id/silver_score"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{user.badgeCounts.silver.toString()}"
android:maxLines="1"
app:layout_constraintLeft_toRightOf="@id/badges_silver"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/badges_silver"
tools:text="3554" />
@@ -109,16 +113,17 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="Bronze - "
android:text="@string/bronze"
app:layout_constraintLeft_toRightOf="@id/divider"
app:layout_constraintTop_toBottomOf="@+id/badges_silver" />
<TextView
android:id="@+id/bronze_score"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{user.badgeCounts.bronze.toString()}"
android:maxLines="1"
app:layout_constraintLeft_toRightOf="@id/badges_bronze"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/badges_bronze"
tools:text="3554" />
@@ -127,17 +132,18 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Date Joined:"
android:text="@string/date_joined"
app:layout_constraintRight_toLeftOf="@id/divider"
app:layout_constraintTop_toBottomOf="@id/badges_bronze" />
<TextView
android:id="@+id/date_joined"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@{ConverterUtil.epochToData(user.creationDate)}"
android:maxLines="1"
app:layout_constraintLeft_toRightOf="@id/divider"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/badges_bronze"
tools:text="31/01/2019" />

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="user"
type="com.example.h_mal.stackexchangeusers.data.network.model.User" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="12dp"
tools:text="12345"
android:text="@{user.userId.toString()}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="12dp"
tools:text="Username"
android:text="@{user.displayName}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/textView"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -5,4 +5,11 @@
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="invalid_entry">Enter a valid username</string>
<string name="username">Username:</string>
<string name="reputation">Reputation:</string>
<string name="bagdes">Bagdes:</string>
<string name="gold">Gold -</string>
<string name="silver">Silver -</string>
<string name="bronze">Bronze -</string>
<string name="date_joined">Date Joined:</string>
</resources>

View File

@@ -1,16 +0,0 @@
package com.example.h_mal.stackexchangeusers
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@@ -0,0 +1,50 @@
package com.example.h_mal.stackexchangeusers.data.network.networkUtils
import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.Assert.*
import org.junit.Test
import retrofit2.Response
import java.io.IOException
import kotlin.test.assertFailsWith
class SafeApiCallTest: SafeApiCall(){
@Test
fun successfulResponse_SuccessfulOutput() = runBlocking{
// GIVEN
val mockApiResponse = mockk<ApiResponse>()
val mockResponse = Response.success(mockApiResponse)
// WHEN
val result = responseUnwrap { mockResponse }
// THEN
assertNotNull(result)
assertEquals(mockApiResponse, result)
}
@Test
fun unsuccessfulResponse_thrownOutput() = runBlocking{
// GIVEN
val errorMessage = "{\n" +
" \"status_code\": 7,\n" +
" \"error_message\": \"Invalid API key: You must be granted a valid key.\",\n" +
"}"
val errorResponseBody = errorMessage.toResponseBody("application/json".toMediaTypeOrNull())
val mockResponse = Response.error<String>(404, errorResponseBody)
//THEN - assert exception is not null
val ioExceptionReturned = assertFailsWith<IOException> {
responseUnwrap { mockResponse }!!
}
assertNotNull(ioExceptionReturned)
assertEquals(ioExceptionReturned.message, "Invalid API key: You must be granted a valid key.")
}
}

View File

@@ -1,10 +1,11 @@
package com.example.h_mal.stackexchangeusers.data.repositories
import com.example.h_mal.stackexchangeusers.data.network.api.ApiClass
import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse
import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse
import com.example.h_mal.stackexchangeusers.data.preferences.PreferenceProvider
import com.example.h_mal.stackexchangeusers.data.room.AppDatabase
import kotlinx.coroutines.runBlocking
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
@@ -15,6 +16,7 @@ import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import retrofit2.Response
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okio.IOException
import kotlin.test.assertFailsWith
@@ -49,44 +51,49 @@ class RepositoryTest {
Mockito.`when`(prefs.getLastSavedAt(input)).thenReturn(null)
// THEN
val getUser = repository.getUsersFromApi(input)
assertNotNull(getUser)
assertEquals(mockApiResponse, getUser)
val apiResponse = repository.getUsersFromApi(input)
assertNotNull(apiResponse)
assertEquals(mockApiResponse, apiResponse)
}
@Test
fun fetchUserFromApi_negativeResponse() = runBlocking {
//GIVEN
//mock retrofit error response
val mockBody = mock(ResponseBody::class.java)
val mockRaw = mock(okhttp3.Response::class.java)
val re = Response.error<String>(mockBody, mockRaw)
val input = "12345"
val errorMessage = "{\n" +
" \"status_code\": 7,\n" +
" \"error_message\": \"Invalid API key: You must be granted a valid key.\",\n" +
"}"
val errorResponseBody = errorMessage.toResponseBody("application/json".toMediaTypeOrNull())
val mockResponse = Response.error<String>(404, errorResponseBody)
//WHEN
Mockito.`when`(api.getUsersFromApi("12345")).then { re }
Mockito.`when`(api.getUsersFromApi(input)).then { mockResponse }
//THEN - assert exception is not null
val ioExceptionReturned = assertFailsWith<IOException> {
repository.getUsersFromApi("12345")
repository.getUsersFromApi(input)
}
assertNotNull(ioExceptionReturned)
assertNotNull(ioExceptionReturned.message)
assertEquals(ioExceptionReturned.message, "Invalid API key: You must be granted a valid key.")
}
@Test
fun fetchUserFromApi_alreadySearched() = runBlocking {
// GIVEN
val input = "12345"
val mockApiResponse = mock(ApiResponse::class.java)
val mockResponse = Response.success(mockApiResponse)
//WHEN
Mockito.`when`(api.getUsersFromApi("12345")).thenReturn(
Mockito.`when`(api.getUsersFromApi(input)).thenReturn(
mockResponse
)
Mockito.`when`(prefs.getLastSavedAt("12345")).thenReturn(System.currentTimeMillis())
Mockito.`when`(prefs.getLastSavedAt(input)).thenReturn(System.currentTimeMillis())
// THEN
val getUser = repository.getUsersFromApi("12345")
val getUser = repository.getUsersFromApi(input)
assertEquals(getUser, null)
}

View File

@@ -2,14 +2,11 @@ package com.example.h_mal.stackexchangeusers.ui.main
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.example.h_mal.stackexchangeusers.data.network.model.ApiResponse
import com.example.h_mal.stackexchangeusers.data.network.model.User
import com.example.h_mal.stackexchangeusers.data.network.response.ApiResponse
import com.example.h_mal.stackexchangeusers.data.repositories.Repository
import com.example.h_mal.stackexchangeusers.data.repositories.RepositoryImpl
import com.example.h_mal.stackexchangeusers.data.room.entities.UserItem
import com.example.h_mal.stackexchangeusers.data.room.entities.UserEntity
import com.example.h_mal.stackexchangeusers.ui.MainViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -22,8 +19,6 @@ import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import java.io.IOException
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class MainViewModelTest {
@@ -38,8 +33,11 @@ class MainViewModelTest {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
viewModel = MainViewModel(repository)
val mockLiveData = object: LiveData<List<UserEntity>>(){}
Mockito.`when`(repository.getUsersFromDatabase()).thenReturn(mockLiveData)
viewModel = MainViewModel(repository)
}
@Test
@@ -52,11 +50,8 @@ class MainViewModelTest {
//THEN
viewModel.getUsers("12345")
viewModel.operationState.observeForever{
it.getContentIfNotHandled()?.let {result ->
assertFalse { result }
}
}
delay(50)
assertFalse { viewModel.operationState.value?.getContentIfNotHandled()!! }
}
@Test
@@ -65,11 +60,8 @@ class MainViewModelTest {
Mockito.`when`(repository.getUsersFromApi("12345")).thenAnswer{ throw IOException("throwed") }
// THEN
viewModel.getUsers("fsdfsdf")
viewModel.operationError.observeForever{
it.getContentIfNotHandled()?.let {result ->
assertEquals(result, "throwed")
}
}
viewModel.getUsers("12345")
delay(50)
assertEquals (viewModel.operationError.value?.getContentIfNotHandled()!!, "throwed")
}
}