Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

부산대 Android 박정훈 6주차 과제 Step2 #72

Open
wants to merge 20 commits into
base: pjhn
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# android-map-notification

## 기능 요구 사항
- 초기 진입 화면을 추가한다
- Firebase의 Remote Config를 설정한다
- 매개변수 serviceState 값이 ON_SERVICE일 때만 초기 진입 화면이 지도 화면으로 넘어간다.
- 매개변수 serviceState 값이 ON_SERVICE이 아닌 경우에는
serviceMessage 값을 초기 진입 화면 하단에 표시하고 지도 화면으로 진입하지 않는다.
- Firebase Cloud Message를 설정한다.
- 테스트 메시지를 보낸다.
- 앱이 백그라운드 상태일 경우 FCM 기본 값을 사용하여 Notification을 발생한다.
- 앱이 포그라운드 상태일 경우 커스텀 Notification을 발생한다.
- Notification 창을 터치하면 초기 진입 화면이 호출된다.

## 프로그래밍 요구 사항
- 서버 상태, UI 로딩 등에 대한 상태 관리를 한다.
- 새로 추가되는 부분에도 MVVM 아키텍처 패턴을 적용한다.
- 코드 컨벤션을 준수하며 프로그래밍한다.


12 changes: 11 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ android {
namespace = "campus.tech.kakao.map"
compileSdk = 34

packagingOptions {
exclude("META-INF/LICENSE-notice.md")
exclude("META-INF/LICENSE.md")
exclude("META-INF/NOTICE.md")
exclude("META-INF/NOTICE")
}

defaultConfig {
resValue("string", "kakao_api_key", getApiKey("KAKAO_API_KEY"))
Expand Down Expand Up @@ -67,7 +73,8 @@ dependencies {
implementation("androidx.room:room-ktx:2.6.1")
implementation("androidx.room:room-runtime:2.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
implementation("com.kakao.sdk:v2-all:2.20.3")
Expand All @@ -81,10 +88,13 @@ dependencies {
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.activity:activity:1.8.0")

testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.13.12")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
implementation("androidx.test:core-ktx:1.6.1")
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
androidTestImplementation("io.mockk:mockk-android:1.13.12")
androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.3.0")
androidTestImplementation("androidx.test.espresso:espresso-intents:3.3.0")
Expand Down
23 changes: 23 additions & 0 deletions app/src/androidTest/java/campus/tech/kakao/map/FakeKakaoApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package campus.tech.kakao.map

import campus.tech.kakao.map.data.net.KakaoApi
import campus.tech.kakao.map.domain.model.Place
import campus.tech.kakao.map.domain.model.ResultSearchKeyword
import retrofit2.Response

class FakeKakaoApi : KakaoApi {
override suspend fun getSearchKeyword(
key: String,
query: String,
size: Int,
page: Int
): Response<ResultSearchKeyword> {
// 가짜 응답 데이터 생성
val fakeDocuments = listOf(
Place("1", "장소 1", "주소 1", "카테고리 1", "0.0", "0.0"),
Place("2", "장소 2", "주소 2", "카테고리 2", "0.0", "0.0")
)
val responseBody = ResultSearchKeyword(documents = fakeDocuments)
return Response.success(responseBody)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package campus.tech.kakao.map

import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import campus.tech.kakao.map.presentation.map.MapActivity
import campus.tech.kakao.map.presentation.search.SearchActivity
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 MapActivityUITest {

@get:Rule
var activityScenarioRule = ActivityScenarioRule(MapActivity::class.java)

@Before
fun setUp() {
Intents.init()
}

@After
fun tearDown() {
Intents.release()
}

@Test
fun `검색_버튼_클릭_시_검색_화면으로_이동`() {

// 자동 호출 된 bottom sheet 닫기
closeBottomSheet()

//When
onView(withId(R.id.btnSearch))
.check(matches(isDisplayed()))
.perform(click())

//Then
Intents.intended(hasComponent(SearchActivity::class.java.name))
}

private fun closeBottomSheet() {
onView(withId(R.id.bottom_sheet))
.perform(click())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package campus.tech.kakao.map

import campus.tech.kakao.map.data.PlaceLocalDataRepository
import campus.tech.kakao.map.data.dao.PlaceDao
import campus.tech.kakao.map.data.entity.PlaceEntity
import campus.tech.kakao.map.domain.model.Place
import io.mockk.Runs
import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifySequence
import io.mockk.just
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test

//장소 데이터 로컬 저장소 테스트
class PlaceLocalDataRepositoryTest {
private lateinit var placeDao: PlaceDao
private lateinit var placeLocalDataRepository: PlaceLocalDataRepository

@Before
fun setup(){
placeDao = mockk()
placeLocalDataRepository = PlaceLocalDataRepository(placeDao)
}

@After
fun tearDown(){
clearAllMocks()
}

@Test
fun testGetPlaces()= runTest {
//Given
val placeEntities = listOf(
PlaceEntity("1", "장소 A", "주소 A", "카테고리 A", "0.0","0.0"),
PlaceEntity("2", "장소 B", "주소 B", "카테고리 B", "0.0","0.0"),
)
coEvery { placeDao.getPlaces("장소") } returns placeEntities

//When
val places = placeLocalDataRepository.getPlaces("장소",1)

//Then
coVerify { placeDao.getPlaces("장소")}
assertEquals(2, places.size)
assertEquals("장소 A", places[0].place)
}

@Test
fun testUpdatePlaces() = runTest{
//Given
val places = listOf(
Place("1", "장소 A", "주소 A", "카테고리 A", "0.0","0.0"),
Place("2", "장소 B", "주소 B", "카테고리 B", "0.0","0.0"),
)
coEvery { placeDao.deleteAllPlaces() } just Runs
coEvery { placeDao.insertPlaces(any()) } just Runs

//When
placeLocalDataRepository.updatePlaces(places)

//Then
coVerifySequence {
placeDao.deleteAllPlaces()
placeDao.insertPlaces(places.map {
PlaceEntity(it.id, it.place, it.address, it.category, it.xPos, it.yPos)
})
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package campus.tech.kakao.map

import campus.tech.kakao.map.data.PlaceRemoteDataRepository
import campus.tech.kakao.map.data.dao.PlaceDao
import campus.tech.kakao.map.domain.model.Place
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

class PlaceRemoteDataRepositoryTest {
private lateinit var placeDao: PlaceDao
private lateinit var kakaoApi: FakeKakaoApi
private lateinit var placeRemoteDataRepository: PlaceRemoteDataRepository

@Before
fun setup(){
placeDao = mockk()
kakaoApi = FakeKakaoApi()
placeRemoteDataRepository = PlaceRemoteDataRepository(placeDao, kakaoApi)
}

@Test
fun testGetPlaces() = runTest {
// Given
val placeName = "장소"
val page = 1

// When
val places: List<Place> = placeRemoteDataRepository.getPlaces(placeName, page)

// Then
assertEquals(2, places.size)
assertEquals("장소 1", places[0].place)
assertEquals("장소 2", places[1].place)
}

}
101 changes: 101 additions & 0 deletions app/src/androidTest/java/campus/tech/kakao/map/SearchActivityUITest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package campus.tech.kakao.map

import android.util.Log
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBackUnconditionally
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import campus.tech.kakao.map.data.LastVisitedPlaceManager
import campus.tech.kakao.map.data.PlaceLocalDataRepository
import campus.tech.kakao.map.data.dao.PlaceDao
import campus.tech.kakao.map.domain.model.Place
import campus.tech.kakao.map.presentation.adapter.LogAdapter
import campus.tech.kakao.map.presentation.adapter.SearchedPlaceAdapter
import campus.tech.kakao.map.presentation.map.MapActivity
import campus.tech.kakao.map.presentation.search.SearchActivity
import campus.tech.kakao.map.presentation.search.SearchViewModel
import io.mockk.clearAllMocks
import io.mockk.mockk
import io.mockk.verify
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.regex.Matcher

class SearchActivityUITest {
private lateinit var lastVisitedPlaceManager: LastVisitedPlaceManager
private lateinit var placeDao: PlaceDao
private lateinit var placeLocalDataRepository: PlaceLocalDataRepository

@get:Rule
val activityRule = ActivityScenarioRule(SearchActivity::class.java)

@Before
fun setUp() {
lastVisitedPlaceManager = mockk()
placeDao = mockk()
placeLocalDataRepository = PlaceLocalDataRepository(placeDao)
}

@After
fun tearDown() {
clearAllMocks()
}

@Test
fun `장소_리스트_클릭_시_검색_화면_종료`(){
//Given
onView(withId(R.id.edtSearch)).perform(replaceText("부산대학교"))

//debounce 기능 테스트 (0.5초 후 api를 가져옴)
Thread.sleep(3000L)

//When
onView(withId(R.id.recyclerPlace))
.perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0,click()))

//Then
assertTrue(activityRule.scenario.state.isAtLeast(androidx.lifecycle.Lifecycle.State.DESTROYED))
}


private fun setLogMockData(){
activityRule.scenario.onActivity {
it.findViewById<RecyclerView>(R.id.recyclerLog)?.let {
val places = listOf(
Place("1", "장소 A", "주소 A", "카테고리 A", "0.0","0.0"),
Place("2", "장소 B", "주소 B", "카테고리 B", "0.0","0.0"),
)

(it.adapter as LogAdapter).submitList(places)
}
}
}


}
Loading