diff --git a/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt index 1f9690b5..5706208e 100644 --- a/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt +++ b/app/src/androidTest/java/campus/tech/kakao/map/SearchActivityTest.kt @@ -25,6 +25,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +@RunWith(AndroidJUnit4::class) class SearchActivityTest { @get:Rule @@ -48,63 +49,81 @@ class SearchActivityTest { @Test fun `검색창_X누르면_지워진다`() { + // given onView(withId(R.id.search)).perform(typeText("cafe")) + + // when onView(withId(R.id.xmark)).perform(click()) + + // then onView(withId(R.id.search)).check(matches(withText(""))) } -// @Test -// fun `검색결과_누르면_메인액티비티로_이동한다`() { -// // Activity 실행 시점에 RecyclerView의 IdlingResource 등록 -// activityRule.scenario.onActivity { activity -> -// val recyclerView = activity.findViewById(R.id.placeResult) -// idlingResource = RecyclerViewIdlingResource(recyclerView) -// IdlingRegistry.getInstance().register(idlingResource) -// -// // 수동으로 RecyclerView에 데이터 삽입 -// val testData = listOf( -// Document( -// addressName = "서울 강남구 삼성동 159", -// categoryGroupCode = "", -// categoryGroupName = "", -// categoryName = "가정,생활 > 문구,사무용품 > 디자인문구 > 카카오프렌즈", -// distance = "418", -// id = "26338954", -// phone = "02-6002-1880", -// placeName = "카카오프렌즈 코엑스점", -// placeUrl = "http://place.map.kakao.com/26338954", -// roadAddressName = "서울 강남구 영동대로 513", -// x = "127.05902969025047", -// y = "37.51207412593136" -// ) -// ) -// -// val placeAdapter = PlaceAdapter( -// testData, -// LayoutInflater.from(activity), -// object : PlaceAdapter.OnItemClickListener { -// override fun onItemClick(position: Int) { -// val item = placeAdapter.getItem(position) -// val intent = Intent(activity, MainActivity::class.java) -// intent.putExtra("longitude", item.x) -// intent.putExtra("latitude", item.y) -// intent.putExtra("name", item.placeName) -// intent.putExtra("address", item.addressName) -// activity.startActivity(intent) -// } -// }) -// -// recyclerView.adapter = placeAdapter -// placeAdapter.notifyDataSetChanged() -// } -// -// // RecyclerView 데이터 로드 확인 -// onView(withId(R.id.placeResult)).check(matches(hasMinimumChildCount(1))) -// -// // 첫 번째 아이템 클릭 -// onView(withId(R.id.placeResult)).perform(actionOnItemAtPosition(0, click())) -// -// // MainActivity로 이동 확인 -// Intents.intended(hasComponent(MainActivity::class.java.name)) -// } -} \ No newline at end of file + @Test + fun 검색결과_누르면_메인액티비티로_이동한다() { + // given + val testData = listOf( + Document( + addressName = "서울 강남구 삼성동 159", + categoryGroupCode = "", + categoryGroupName = "", + categoryName = "가정,생활 > 문구,사무용품 > 디자인문구 > 카카오프렌즈", + distance = "418", + id = "26338954", + phone = "02-6002-1880", + placeName = "카카오프렌즈 코엑스점", + placeUrl = "http://place.map.kakao.com/26338954", + roadAddressName = "서울 강남구 영동대로 513", + x = "127.05902969025047", + y = "37.51207412593136" + ) + ) + + // when + activityRule.scenario.onActivity { activity -> + val recyclerView = activity.findViewById(R.id.placeResult) + + // 커스텀 IdlingResource 생성 및 등록 + idlingResource = object : IdlingResource { + private var resourceCallback: IdlingResource.ResourceCallback? = null + override fun getName(): String = "RecyclerView Idling Resource" + override fun isIdleNow(): Boolean = recyclerView.adapter?.itemCount ?: 0 > 0 + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { + resourceCallback = callback + } + } + IdlingRegistry.getInstance().register(idlingResource) + + // 데이터 설정 + val placeAdapter = PlaceAdapter( + testData, + LayoutInflater.from(activity), + object : PlaceAdapter.OnItemClickListener { + override fun onItemClick(position: Int) { + val item = testData[position] + val intent = Intent(activity, MainActivity::class.java) + intent.putExtra("longitude", item.x) + intent.putExtra("latitude", item.y) + intent.putExtra("name", item.placeName) + intent.putExtra("address", item.addressName) + activity.startActivity(intent) + } + }) + + recyclerView.adapter = placeAdapter + placeAdapter.notifyDataSetChanged() + } + + // 대기 + IdlingRegistry.getInstance().resources.forEach { + if (!it.isIdleNow) { + Thread.sleep(1000) // 1초 대기 + } + } + + // then + onView(withId(R.id.placeResult)).check(matches(hasMinimumChildCount(1))) + onView(withId(R.id.placeResult)).perform(actionOnItemAtPosition(0, click())) + Intents.intended(hasComponent(MainActivity::class.java.name)) + } +} diff --git a/app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt b/app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt index b5a95b7f..f9e52c4b 100644 --- a/app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/SearchViewModel.kt @@ -1,7 +1,6 @@ package campus.tech.kakao.map import android.content.Context -import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -11,8 +10,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class SearchViewModel(context: Context) : ViewModel() { - private val dbHelper: DBHelper = DBHelper(context) - private val db = dbHelper.writableDatabase private val preferenceManager = MapApplication.prefs var repository = RetrofitRepository() @@ -33,40 +30,6 @@ class SearchViewModel(context: Context) : ViewModel() { val locationList: LiveData> get() = _locationList - fun insertPlace(place: Place) { - dbHelper.insert(db, place) - } - - override fun onCleared() { - super.onCleared() - if (db.isOpen) db.close() - } - - fun getSearchResult(searchText: String) { - if (searchText.isEmpty()) { - _placeList.postValue(emptyList()) - } else { - val rDb = dbHelper.readableDatabase - val places = mutableListOf() - val query = "SELECT * FROM ${PlaceContract.TABLE_NAME} WHERE ${PlaceContract.TABLE_COLUMN_NAME} LIKE ?" - val cursor = rDb.rawQuery(query, arrayOf("%$searchText%")) - - if (cursor != null) { - if (cursor.moveToFirst()) { - do { - val name = cursor.getString(cursor.getColumnIndexOrThrow(PlaceContract.TABLE_COLUMN_NAME)) - val address = cursor.getString(cursor.getColumnIndexOrThrow(PlaceContract.TABLE_COLUMN_ADDRESS)) - val category = cursor.getString(cursor.getColumnIndexOrThrow(PlaceContract.TABLE_COLUMN_CATEGORY)) - val place = Place(name, address, category) - places.add(place) - } while (cursor.moveToNext()) - } - cursor.close() - } - _placeList.postValue(places) - } - } - fun getSearchHistoryList() { _searchHistoryList.value = getSearchHistory() } diff --git a/app/src/test/java/campus/tech/kakao/map/SearchViewModelTest.kt b/app/src/test/java/campus/tech/kakao/map/SearchViewModelTest.kt index 6ce6d841..a62a6716 100644 --- a/app/src/test/java/campus/tech/kakao/map/SearchViewModelTest.kt +++ b/app/src/test/java/campus/tech/kakao/map/SearchViewModelTest.kt @@ -1,182 +1,241 @@ -//package campus.tech.kakao.map -// -//import android.content.Context -//import androidx.arch.core.executor.testing.InstantTaskExecutorRule -//import androidx.lifecycle.Observer -//import androidx.test.core.app.ApplicationProvider -//import androidx.test.ext.junit.runners.AndroidJUnit4 -//import kotlinx.coroutines.test.UnconfinedTestDispatcher -//import kotlinx.coroutines.test.advanceUntilIdle -//import kotlinx.coroutines.test.runTest -//import org.junit.* -//import org.junit.Assert.* -//import org.junit.runner.RunWith -//import org.robolectric.annotation.Config -// -//@RunWith(AndroidJUnit4::class) -//@Config(manifest = Config.NONE) -//class SearchViewModelTest { -// -// @get:Rule -// val instantTaskExecutorRule = InstantTaskExecutorRule() -// -// private lateinit var viewModel: SearchViewModel -// private lateinit var context: Context -// private lateinit var preferenceManager: FakePreferenceManager -// private lateinit var repository: FakeRetrofitRepository -// -// @Before -// fun setUp() { -// context = ApplicationProvider.getApplicationContext() -// preferenceManager = FakePreferenceManager(context) -// repository = FakeRetrofitRepository() -// MapApplication.prefs = preferenceManager -// -// viewModel = SearchViewModel(context) -// viewModel.repository = repository // repository를 설정합니다. -// } -// -// @Test -// fun getSearchHistoryList_setsSearchHistory() { -// val document = Document( -// addressName = "Address1", -// categoryGroupCode = "CategoryGroupCode1", -// categoryGroupName = "CategoryGroupName1", -// categoryName = "CategoryName1", -// distance = "Distance1", -// id = "Id1", -// phone = "Phone1", -// placeName = "Place1", -// placeUrl = "PlaceUrl1", -// roadAddressName = "RoadAddressName1", -// x = "X1", -// y = "Y1" -// ) -// val searchHistory = SearchHistory("Search1", document) -// preferenceManager.addSearchHistory(searchHistory) -// -// val observer = Observer> {} -// viewModel.searchHistoryList.observeForever(observer) -// -// viewModel.getSearchHistoryList() -// assertEquals(1, viewModel.searchHistoryList.value!!.size) -// assertEquals("Search1", viewModel.searchHistoryList.value!![0].searchHistory) -// } -// -// @Test -// fun saveSearchHistory_updatesSearchHistory() { -// val document = Document( -// addressName = "Address1", -// categoryGroupCode = "CategoryGroupCode1", -// categoryGroupName = "CategoryGroupName1", -// categoryName = "CategoryName1", -// distance = "Distance1", -// id = "Id1", -// phone = "Phone1", -// placeName = "Place1", -// placeUrl = "PlaceUrl1", -// roadAddressName = "RoadAddressName1", -// x = "X1", -// y = "Y1" -// ) -// val searchHistory = SearchHistory("Search1", document) -// -// viewModel.saveSearchHistory(searchHistory) -// -// assertEquals(1, preferenceManager.getArrayList(Constants.SEARCH_HISTORY_KEY).size) -// assertEquals("Search1", preferenceManager.getArrayList(Constants.SEARCH_HISTORY_KEY)[0].searchHistory) -// } -// -// @Test -// fun deleteSearchHistory_removesItemFromSearchHistory() { -// val document = Document( -// addressName = "Address1", -// categoryGroupCode = "CategoryGroupCode1", -// categoryGroupName = "CategoryGroupName1", -// categoryName = "CategoryName1", -// distance = "Distance1", -// id = "Id1", -// phone = "Phone1", -// placeName = "Place1", -// placeUrl = "PlaceUrl1", -// roadAddressName = "RoadAddressName1", -// x = "X1", -// y = "Y1" -// ) -// val searchHistory = SearchHistory("Search1", document) -// preferenceManager.addSearchHistory(searchHistory) -// -// viewModel.deleteSearchHistory(0) -// -// assertTrue(preferenceManager.getArrayList(Constants.SEARCH_HISTORY_KEY).isEmpty()) -// } -// -// @Test -// fun getPlace_setsLocationList() = runTest(UnconfinedTestDispatcher()) { -// val searchText = "Place" -// val documents = listOf( -// Document( -// addressName = "Address1", -// categoryGroupCode = "CategoryGroupCode1", -// categoryGroupName = "CategoryGroupName1", -// categoryName = "CategoryName1", -// distance = "Distance1", -// id = "Id1", -// phone = "Phone1", -// placeName = "Place1", -// placeUrl = "PlaceUrl1", -// roadAddressName = "RoadAddressName1", -// x = "X1", -// y = "Y1" -// ) -// ) -// repository.setPlaces(documents) -// -// val observer = Observer> {} -// viewModel.locationList.observeForever(observer) -// -// viewModel.getPlace(searchText) -// advanceUntilIdle() -// -// assertEquals(1, viewModel.locationList.value!!.size) -// assertEquals("Place1", viewModel.locationList.value!![0].placeName) -// } -//} -// -//class FakePreferenceManager(context: Context) : PreferenceManager(context) { -// private val searchHistory = mutableListOf() -// -// override fun getArrayList(key: String): ArrayList { -// return ArrayList(searchHistory) -// } -// -// override fun savePreference(key: String, searchHistory: SearchHistory, currentList: ArrayList) { -// this.searchHistory.add(searchHistory) -// } -// -// override fun deleteArrayListItem(key: String, index: Int) { -// if (index >= 0 && index < searchHistory.size) { -// searchHistory.removeAt(index) -// } -// } -// -// fun addSearchHistory(searchHistory: SearchHistory) { -// this.searchHistory.add(searchHistory) -// } -// -// fun clearSearchHistory() { -// this.searchHistory.clear() -// } -//} -// -//class FakeRetrofitRepository : RetrofitRepository() { -// private var places = listOf() -// -// fun setPlaces(places: List) { -// this.places = places -// } -// -// override suspend fun getPlace(query: String): List { -// return places -// } -//} +package campus.tech.kakao.map + +import android.content.Context +import android.os.Looper +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.* +import org.junit.* +import org.junit.Assert.* +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.robolectric.Shadows +import org.robolectric.annotation.Config +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +@RunWith(AndroidJUnit4::class) +@Config(manifest = Config.NONE) +class SearchViewModelTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainCoroutineRule = MainCoroutineRule() + + private lateinit var viewModel: SearchViewModel + private lateinit var context: Context + private lateinit var preferenceManager: FakePreferenceManager + private lateinit var repository: FakeRetrofitRepository + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + preferenceManager = FakePreferenceManager(context) + repository = FakeRetrofitRepository() + MapApplication.prefs = preferenceManager + + viewModel = SearchViewModel(context) + viewModel.repository = repository + } + + @Test + fun getSearchHistoryList_setsSearchHistory() { + // given + val document = Document( + addressName = "Address1", + categoryGroupCode = "CategoryGroupCode1", + categoryGroupName = "CategoryGroupName1", + categoryName = "CategoryName1", + distance = "Distance1", + id = "Id1", + phone = "Phone1", + placeName = "Place1", + placeUrl = "PlaceUrl1", + roadAddressName = "RoadAddressName1", + x = "X1", + y = "Y1" + ) + val searchHistory = SearchHistory("Search1", document) + preferenceManager.addSearchHistory(searchHistory) + + val observer = Observer> {} + viewModel.searchHistoryList.observeForever(observer) + + // when + viewModel.getSearchHistoryList() + + // then + assertEquals(1, viewModel.searchHistoryList.value!!.size) + assertEquals("Search1", viewModel.searchHistoryList.value!![0].searchHistory) + } + + @Test + fun saveSearchHistory_updatesSearchHistory() { + // given + val document = Document( + addressName = "Address1", + categoryGroupCode = "CategoryGroupCode1", + categoryGroupName = "CategoryGroupName1", + categoryName = "CategoryName1", + distance = "Distance1", + id = "Id1", + phone = "Phone1", + placeName = "Place1", + placeUrl = "PlaceUrl1", + roadAddressName = "RoadAddressName1", + x = "X1", + y = "Y1" + ) + val searchHistory = SearchHistory("Search1", document) + + // when + viewModel.saveSearchHistory(searchHistory) + + // then + assertEquals(1, preferenceManager.getArrayList(Constants.SEARCH_HISTORY_KEY).size) + assertEquals("Search1", preferenceManager.getArrayList(Constants.SEARCH_HISTORY_KEY)[0].searchHistory) + } + + @Test + fun deleteSearchHistory_removesItemFromSearchHistory() { + // given + val document = Document( + addressName = "Address1", + categoryGroupCode = "CategoryGroupCode1", + categoryGroupName = "CategoryGroupName1", + categoryName = "CategoryName1", + distance = "Distance1", + id = "Id1", + phone = "Phone1", + placeName = "Place1", + placeUrl = "PlaceUrl1", + roadAddressName = "RoadAddressName1", + x = "X1", + y = "Y1" + ) + val searchHistory = SearchHistory("Search1", document) + preferenceManager.addSearchHistory(searchHistory) + + // when + viewModel.deleteSearchHistory(0) + + // then + assertTrue(preferenceManager.getArrayList(Constants.SEARCH_HISTORY_KEY).isEmpty()) + } + + @Test + fun getPlace_setsLocationList() = runTest { + // given + val searchText = "Place" + val documents = listOf( + Document( + addressName = "Address1", + categoryGroupCode = "CategoryGroupCode1", + categoryGroupName = "CategoryGroupName1", + categoryName = "CategoryName1", + distance = "Distance1", + id = "Id1", + phone = "Phone1", + placeName = "Place1", + placeUrl = "PlaceUrl1", + roadAddressName = "RoadAddressName1", + x = "X1", + y = "Y1" + ) + ) + repository.setPlaces(documents) + + // when + viewModel.getPlace(searchText) + + // then + advanceUntilIdle() // Wait for all coroutines to complete + val result = viewModel.locationList.getOrAwaitValue() + + assertEquals(1, result.size) + assertEquals("Place1", result[0].placeName) + } +} + +class FakePreferenceManager(context: Context) : PreferenceManager(context) { + private val searchHistory = mutableListOf() + + override fun getArrayList(key: String): ArrayList { + return ArrayList(searchHistory) + } + + override fun savePreference(key: String, searchHistory: SearchHistory, currentList: ArrayList) { + this.searchHistory.add(searchHistory) + } + + override fun deleteArrayListItem(key: String, index: Int) { + if (index >= 0 && index < searchHistory.size) { + searchHistory.removeAt(index) + } + } + + fun addSearchHistory(searchHistory: SearchHistory) { + this.searchHistory.add(searchHistory) + } + + fun clearSearchHistory() { + this.searchHistory.clear() + } +} + +class FakeRetrofitRepository : RetrofitRepository() { + private var places = listOf() + + fun setPlaces(places: List) { + this.places = places + } + + override suspend fun getPlace(query: String): List { + return places + } +} + +fun LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS +): T { + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(value: T) { + data = value + latch.countDown() + this@getOrAwaitValue.removeObserver(this) + } + } + + this.observeForever(observer) + + if (!latch.await(time, timeUnit)) { + throw TimeoutException("LiveData value was never set.") + } + + @Suppress("UNCHECKED_CAST") + return data as T +} + +class MainCoroutineRule( + private val dispatcher: TestDispatcher = UnconfinedTestDispatcher() +) : TestWatcher() { + override fun starting(description: Description) { + Dispatchers.setMain(dispatcher) + } + + override fun finished(description: Description) { + Dispatchers.resetMain() + } +} \ No newline at end of file