mirror of
https://github.com/hmalik144/Candy_Space_tech_test.git
synced 2025-12-10 03:05:27 +00:00
Unit test for repository added
This commit is contained in:
@@ -42,8 +42,9 @@ dependencies {
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
//mockito
|
||||
//mockito and livedata testing
|
||||
testImplementation 'org.mockito:mockito-inline:2.13.0'
|
||||
implementation 'android.arch.core:core-testing'
|
||||
|
||||
//Retrofit and GSON
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity android:name=".ui.MainActivity">
|
||||
<activity android:name=".ui.main.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.example.h_mal.candyspace.data.api
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import com.example.h_mal.candyspace.data.api.model.ApiResponse
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
@@ -15,17 +14,21 @@ interface ApiClass {
|
||||
@GET("users?")
|
||||
suspend fun getUsersFromApi(@Query("inname") inname: String): Response<ApiResponse>
|
||||
|
||||
//invoke method creating an invocation of the api call
|
||||
companion object{
|
||||
operator fun invoke(
|
||||
//injected @params
|
||||
networkConnectionInterceptor: NetworkConnectionInterceptor,
|
||||
queryParamsInterceptor: QueryParamsInterceptor
|
||||
) : ApiClass{
|
||||
|
||||
//okHttpClient with interceptors
|
||||
val okkHttpclient = OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(networkConnectionInterceptor)
|
||||
.addInterceptor(queryParamsInterceptor)
|
||||
.build()
|
||||
|
||||
//retrofit to be used in @Repository
|
||||
return Retrofit.Builder()
|
||||
.client(okkHttpclient)
|
||||
.baseUrl("https://api.stackexchange.com/2.2/")
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
package com.example.h_mal.candyspace.data.api
|
||||
package com.example.h_mal.candyspace.data.api.model
|
||||
|
||||
import com.example.h_mal.candyspace.data.api.model.User
|
||||
|
||||
data class ApiResponse(
|
||||
val items : List<User>?,
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.h_mal.candyspace.data.api
|
||||
package com.example.h_mal.candyspace.data.api.model
|
||||
|
||||
import com.google.gson.annotations.Expose
|
||||
import com.google.gson.annotations.SerializedName
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.h_mal.candyspace.data.api
|
||||
package com.example.h_mal.candyspace.data.api.model
|
||||
|
||||
import com.google.gson.annotations.Expose
|
||||
import com.google.gson.annotations.SerializedName
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.example.h_mal.candyspace.data.repositories
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.example.h_mal.candyspace.data.api.ApiClass
|
||||
import com.example.h_mal.candyspace.data.api.ApiResponse
|
||||
import com.example.h_mal.candyspace.data.api.model.ApiResponse
|
||||
import com.example.h_mal.candyspace.data.api.ResponseUnwrap
|
||||
import com.example.h_mal.candyspace.data.api.User
|
||||
|
||||
class Repository(
|
||||
private val api: ApiClass
|
||||
): ResponseUnwrap() {
|
||||
|
||||
//get api response from retrofit class
|
||||
//then unwrap data object from retrofit response class
|
||||
suspend fun getUsers(username: String): ApiResponse {
|
||||
return responseUnwrap { api.getUsersFromApi(username) }
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
package com.example.h_mal.candyspace.ui.main
|
||||
package com.example.h_mal.candyspace.ui.home
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.example.h_mal.candyspace.R
|
||||
import com.example.h_mal.candyspace.data.api.User
|
||||
import com.example.h_mal.candyspace.data.api.model.User
|
||||
import com.example.h_mal.candyspace.databinding.MainFragmentBinding
|
||||
import com.example.h_mal.candyspace.ui.MainActivity.Companion.viewModel
|
||||
import com.example.h_mal.candyspace.utils.displayToast
|
||||
import com.example.h_mal.candyspace.ui.main.MainActivity.Companion.viewModel
|
||||
import com.example.h_mal.candyspace.ui.user.ListItemViewModel
|
||||
import com.example.h_mal.candyspace.ui.user.UserProfileFragment
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.ViewHolder
|
||||
|
||||
|
||||
/*
|
||||
* UI for the first screen holding the list, search box
|
||||
*/
|
||||
/**
|
||||
* UI for the first screen holding the list, search box
|
||||
*/
|
||||
class MainFragment : Fragment(){
|
||||
|
||||
companion object {
|
||||
@@ -41,26 +41,37 @@ class MainFragment : Fragment(){
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
//observer the live data of what is retrieved from the API call
|
||||
viewModel.usersLiveData.observe(viewLifecycleOwner, Observer {
|
||||
//create adapter for viewbinding into the recycler view
|
||||
val mAdapter = GroupAdapter<ViewHolder>().apply {
|
||||
addAll(it.toUserViewModels())
|
||||
}
|
||||
|
||||
//setup the recyclerview
|
||||
binding.recyclerView.apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
setHasFixedSize(true)
|
||||
adapter = mAdapter
|
||||
}
|
||||
|
||||
/*
|
||||
* Item click listener for recyclerView
|
||||
*
|
||||
*/
|
||||
mAdapter.setOnItemClickListener { item, _ ->
|
||||
//get the position of the item clicked
|
||||
val i = mAdapter.getAdapterPosition(item)
|
||||
|
||||
|
||||
//set user in @MainViewModel
|
||||
viewModel.setCurrentUser(it[i])
|
||||
|
||||
/**
|
||||
* display [UserProfileFragment]
|
||||
*/
|
||||
activity!!.supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, UserProfileFragment())
|
||||
.replace(R.id.container,
|
||||
UserProfileFragment()
|
||||
)
|
||||
.addToBackStack("user")
|
||||
.commit()
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.example.h_mal.candyspace.ui.main
|
||||
|
||||
/**
|
||||
* completion listener for [MainViewModel] when handling async calls
|
||||
*/
|
||||
interface CompletionListener {
|
||||
fun onStarted()
|
||||
fun onSuccess()
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
package com.example.h_mal.candyspace.ui
|
||||
package com.example.h_mal.candyspace.ui.main
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.example.h_mal.candyspace.R
|
||||
import com.example.h_mal.candyspace.ui.main.CompletionListener
|
||||
import com.example.h_mal.candyspace.ui.main.MainFragment
|
||||
import com.example.h_mal.candyspace.ui.main.MainViewModel
|
||||
import com.example.h_mal.candyspace.ui.main.MainViewModelFactory
|
||||
import com.example.h_mal.candyspace.ui.home.MainFragment
|
||||
import com.example.h_mal.candyspace.utils.displayToast
|
||||
import com.example.h_mal.candyspace.utils.hide
|
||||
import com.example.h_mal.candyspace.utils.show
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.KodeinAware
|
||||
import org.kodein.di.android.kodein
|
||||
import org.kodein.di.generic.instance
|
||||
|
||||
/**
|
||||
* [MainActivity] hosting the fragments and controlling a lot of the UI
|
||||
*/
|
||||
class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener {
|
||||
|
||||
//retrieve the viewmodel factory from the kodein dependency injection
|
||||
@@ -35,6 +33,7 @@ class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.main_activity)
|
||||
|
||||
//setup home button for back navigation
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true);
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true);
|
||||
|
||||
@@ -43,11 +42,15 @@ class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener {
|
||||
viewModel.completionListener = this
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
//display first fragment
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, MainFragment())
|
||||
.replace(R.id.container,
|
||||
MainFragment()
|
||||
)
|
||||
.commit()
|
||||
}
|
||||
|
||||
//fragment change listener to display title based on current fragment
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
val name = when (supportFragmentManager.fragments[0]::class.java.simpleName) {
|
||||
"UserProfileFragment" -> "User"
|
||||
@@ -57,6 +60,11 @@ class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Back button over override
|
||||
* - close app if home fragment
|
||||
* - return to previous fragment if User fragment
|
||||
*/
|
||||
override fun onBackPressed() {
|
||||
if(supportFragmentManager.backStackEntryCount > 0){
|
||||
supportFragmentManager.popBackStack()
|
||||
@@ -66,6 +74,7 @@ class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener {
|
||||
|
||||
}
|
||||
|
||||
//When home button in toolbar is pressed
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> onBackPressed()
|
||||
@@ -73,15 +82,21 @@ class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener {
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/*
|
||||
* completion listener methods
|
||||
*/
|
||||
override fun onStarted() {
|
||||
//show loading
|
||||
progress_circular.show()
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
//loading completed - hide loading
|
||||
progress_circular.hide()
|
||||
}
|
||||
|
||||
override fun onFailure(message: String) {
|
||||
//loading failed - hide loading and display toast of error
|
||||
progress_circular.hide()
|
||||
displayToast(message)
|
||||
}
|
||||
@@ -3,15 +3,10 @@ package com.example.h_mal.candyspace.ui.main
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.example.h_mal.candyspace.R
|
||||
import com.example.h_mal.candyspace.data.api.User
|
||||
import com.example.h_mal.candyspace.data.api.model.User
|
||||
import com.example.h_mal.candyspace.data.repositories.Repository
|
||||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -21,29 +16,39 @@ class MainViewModel(
|
||||
private val repository: Repository
|
||||
) : ViewModel() {
|
||||
|
||||
// string binded to the edittext input in @R.layout.main_fragment
|
||||
var searchString: String? = null
|
||||
|
||||
var completionListener: CompletionListener? = null
|
||||
|
||||
//data objects - live data and var
|
||||
val usersLiveData = MutableLiveData<List<User>>()
|
||||
var currentUserLiveData: User? = null
|
||||
|
||||
fun submit(view: View){
|
||||
/**
|
||||
* view binding of the submit button in @R.layout.main_fragment
|
||||
*/
|
||||
fun submit(view: View?){
|
||||
//close keyboard when clicked
|
||||
view.let { v ->
|
||||
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
val imm = view?.context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.hideSoftInputFromWindow(v?.windowToken, 0)
|
||||
}
|
||||
|
||||
completionListener?.onStarted()
|
||||
//return if search string is empty
|
||||
if (searchString.isNullOrEmpty()){
|
||||
completionListener?.onFailure("Search box is empty")
|
||||
return
|
||||
}
|
||||
|
||||
//open a coroutine on the Main thread and update views upon load
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
//retrieve response from API call
|
||||
val apiResponse = repository.getUsers(searchString!!)
|
||||
|
||||
//unwrap list of user out of ApiResponse
|
||||
apiResponse.items?.let {
|
||||
if (it.isNotEmpty()){
|
||||
//update live data
|
||||
|
||||
@@ -4,6 +4,10 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.example.h_mal.candyspace.data.repositories.Repository
|
||||
|
||||
/**
|
||||
* Viewmodel factory for [MainViewModel]
|
||||
* @repository injected into MainViewModel
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class MainViewModelFactory(
|
||||
private val repository: Repository
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.example.h_mal.candyspace.ui.main
|
||||
package com.example.h_mal.candyspace.ui.user
|
||||
|
||||
import com.example.h_mal.candyspace.R
|
||||
import com.example.h_mal.candyspace.data.api.User
|
||||
import com.example.h_mal.candyspace.data.api.model.User
|
||||
import com.example.h_mal.candyspace.databinding.ListItemLayoutBinding
|
||||
import com.xwray.groupie.databinding.BindableItem
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package com.example.h_mal.candyspace.ui.main
|
||||
package com.example.h_mal.candyspace.ui.user
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.Observer
|
||||
|
||||
import com.example.h_mal.candyspace.R
|
||||
import com.example.h_mal.candyspace.databinding.FragmentUserProfileBinding
|
||||
import com.example.h_mal.candyspace.ui.MainActivity
|
||||
import com.example.h_mal.candyspace.ui.MainActivity.Companion.viewModel
|
||||
import com.example.h_mal.candyspace.ui.main.MainActivity.Companion.viewModel
|
||||
import com.squareup.picasso.Picasso
|
||||
|
||||
/**
|
||||
@@ -20,7 +18,8 @@ import com.squareup.picasso.Picasso
|
||||
class UserProfileFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
fun newInstance() = UserProfileFragment()
|
||||
fun newInstance() =
|
||||
UserProfileFragment()
|
||||
}
|
||||
|
||||
lateinit var binding: FragmentUserProfileBinding
|
||||
@@ -29,13 +28,13 @@ class UserProfileFragment : Fragment() {
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
activity?.actionBar?.setHomeButtonEnabled(true)
|
||||
// Inflate the layout for this fragment
|
||||
// Inflate the layout for this fragment into data binding
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_user_profile, container, false)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
//Update the data for viewbinding onResume as data would have changed when selecting a new user
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.currentUserLiveData?.let {
|
||||
@@ -2,14 +2,14 @@
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".ui.main.UserProfileFragment">
|
||||
tools:context=".ui.user.UserProfileFragment">
|
||||
|
||||
<data>
|
||||
<import type="com.example.h_mal.candyspace.R"/>
|
||||
<import type="com.example.h_mal.candyspace.utils.ConverterUtil"/>
|
||||
<variable
|
||||
name="user"
|
||||
type="com.example.h_mal.candyspace.data.api.User" />
|
||||
type="com.example.h_mal.candyspace.data.api.model.User" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<data>
|
||||
<variable
|
||||
name="user"
|
||||
type="com.example.h_mal.candyspace.data.api.User" />
|
||||
type="com.example.h_mal.candyspace.data.api.model.User" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.MainActivity" >
|
||||
tools:context=".ui.main.MainActivity" >
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_circular"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.main.MainFragment">
|
||||
tools:context=".ui.home.MainFragment">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.example.h_mal.candyspace.data.repositories
|
||||
|
||||
import com.example.h_mal.candyspace.data.api.ApiClass
|
||||
import com.example.h_mal.candyspace.data.api.ApiResponse
|
||||
import com.example.h_mal.candyspace.data.api.User
|
||||
import com.example.h_mal.candyspace.data.api.model.ApiResponse
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.ResponseBody
|
||||
|
||||
@@ -1,12 +1,58 @@
|
||||
package com.example.h_mal.candyspace.ui.main
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.example.h_mal.candyspace.data.api.ApiClass
|
||||
import com.example.h_mal.candyspace.data.api.model.ApiResponse
|
||||
import com.example.h_mal.candyspace.data.api.model.User
|
||||
import com.example.h_mal.candyspace.data.repositories.Repository
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
class MainViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
var rule: TestRule = InstantTaskExecutorRule()
|
||||
|
||||
lateinit var viewModel: MainViewModel
|
||||
|
||||
@Mock
|
||||
lateinit var repository: Repository
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
viewModel = MainViewModel(repository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getApiFromRepository_SuccessfulReturn() = runBlocking{
|
||||
val user = mock(User::class.java)
|
||||
val mockApiResponse = ApiResponse(listOf(user),null,null,null)
|
||||
|
||||
Mockito.`when`(repository.getUsers("12345")).thenReturn(mockApiResponse)
|
||||
|
||||
viewModel.submit(null)
|
||||
|
||||
assertEquals(mockApiResponse.items, viewModel.usersLiveData.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getApiFromRepository_unsuccessfulReturn() = runBlocking{
|
||||
Mockito.`when`(repository.getUsers("12345")).thenAnswer{ throw IOException() }
|
||||
|
||||
viewModel.submit(null)
|
||||
|
||||
assertEquals(null, viewModel.usersLiveData.value)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user