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

refactor: feature/#558-apply-calldapter 테스트용 PR #573

Closed
wants to merge 53 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
4e11110
refactor: ResponseResult interface와 응답 class 분리
hxeyexn Nov 28, 2024
c992a87
refactor: Exception message 기본 인자 설정
hxeyexn Nov 28, 2024
d17777f
refactor: 204 응답 처리 로직 제거
hxeyexn Nov 28, 2024
3f60c5b
feat: NetworkResultCall 구현
hxeyexn Nov 28, 2024
eb2ef9f
refactor: status code, exception 메시지 상수화
hxeyexn Nov 28, 2024
ffe84f1
feat: NetworkResultCallAdapter 구현
hxeyexn Nov 28, 2024
2ce3a7d
feat: CallAdapterFactory 구현
hxeyexn Nov 28, 2024
431975d
feat: Retrofit 초기화 시 CallAdapterFactory 추가
hxeyexn Nov 28, 2024
b058b24
refactor: 타임라인 기능 CallAdapter 적용
hxeyexn Nov 28, 2024
d2e5e9d
refactor: 카테고리(전 추억) 기능 CallAdapter 적용
hxeyexn Nov 28, 2024
84b464f
refactor: 스타카토 기능 CallAdapter 적용
hxeyexn Nov 28, 2024
535a3e9
refactor: 댓글 기능 CallAdapter 적용
hxeyexn Nov 28, 2024
b1e90fa
refactor: 이미지 업로드 기능 CallAdapter 적용
hxeyexn Nov 28, 2024
b334d4a
refactor: 마이페이지 기능 CallAdapter 적용
hxeyexn Nov 28, 2024
5c815fc
refactor: 로그인 기능 CallAdapter 적용
hxeyexn Nov 28, 2024
fc26942
refactor: 멤버 기능 CallAdapter 적용
hxeyexn Nov 28, 2024
7501253
refactor: 이전 handleApiResponse 제거
hxeyexn Nov 28, 2024
3b534c2
refactor: handleApiResponse2 이름 변경
hxeyexn Nov 28, 2024
9b9ad03
refactor: ResponseResult -> ApiResult로 이름 변경
hxeyexn Nov 28, 2024
f846206
refactor: ApiResponseHandler 이름 변경
hxeyexn Nov 28, 2024
b32ecc8
refactor: NetworkResultCall 이름 변경
hxeyexn Nov 28, 2024
be27563
refactor: NetworkResultCallAdapter 이름 변경
hxeyexn Nov 28, 2024
7419d07
refactor: NetworkResultCallAdapterFactory 이름 변경
hxeyexn Nov 28, 2024
8fcf08d
refactor: ApiResult 처리 로직 추가
hxeyexn Dec 1, 2024
a9b13eb
refactor: 카테고리 리포지터리 ApiResult 처리 확장 함수 적용
hxeyexn Dec 1, 2024
63a9d7f
refactor: 스타카토 리포지터리 ApiResult 처리 확장 함수 적용
hxeyexn Dec 7, 2024
d01b05c
refactor: 타임라인 리포지터리 ApiResult 처리 확장 함수 적용
hxeyexn Dec 7, 2024
d5c359c
refactor: 마이페이지 리포지터리 ApiResult 처리 확장 함수 적용
hxeyexn Dec 7, 2024
334d563
refactor: 댓글 리포지터리 ApiResult 처리 확장 함수 적용
hxeyexn Dec 7, 2024
f1605ee
refactor: 멤버 리포지터리 ApiResult 처리 확장 함수 적용
hxeyexn Dec 7, 2024
ca5b48e
refactor: 로그인 리포지터리 ApiResult 처리 확장 함수 적용
hxeyexn Dec 7, 2024
76d4517
refactor: 이미지 리포지터리 ApiResult 처리 확장 함수 적용
hxeyexn Dec 7, 2024
6e7253b
build: coroutines test 의존성 추가
hxeyexn Dec 17, 2024
a59d4d8
build: mockwebserver 의존성 추가
hxeyexn Dec 17, 2024
06ebf59
build: JUnit5 의존성 추가
hxeyexn Dec 17, 2024
202f50c
feat: CoroutinesTestExtension 추가
hxeyexn Dec 17, 2024
ea6d815
test: api 요청 성공(200) 시 CallAdapter 동작 테스트 추가
hxeyexn Dec 18, 2024
037b169
test: api 요청 실패(400) 시 CallAdapter 동작 테스트 추가
hxeyexn Dec 18, 2024
1ecc3d8
test: MockResponse 생성 로직 함수로 분리
hxeyexn Dec 18, 2024
7f398af
test: api 요청 실패(413) 시 CallAdapter 동작 테스트 추가
hxeyexn Dec 18, 2024
bead6ca
test: api 요청 실패(예외 발생) 시 CallAdapter 동작 테스트 추가
hxeyexn Dec 18, 2024
0ff8359
test: api 요청 실패(500) 시 CallAdapter 동작 테스트 추가
hxeyexn Dec 18, 2024
ae28100
test: api 요청 실패(403) 시 CallAdapter 동작 테스트 추가
hxeyexn Dec 18, 2024
2e021d6
test: api 요청 실패(401) 시 CallAdapter 동작 테스트 추가
hxeyexn Dec 18, 2024
908db7e
test: api 요청 성공(200) CallAdapter 테스트 상태 코드 201로 수정
hxeyexn Dec 18, 2024
10f23b6
test: api 요청 성공(200) 시 CallAdapter 테스트 추가
hxeyexn Dec 18, 2024
3f41b05
test: api 응답 TestFixture 추가
hxeyexn Dec 18, 2024
8e1e687
refactor: CallAdapterTest 이름 변경
hxeyexn Dec 18, 2024
7768209
refactor: request test fixture 변수를 함수로 변경
hxeyexn Dec 18, 2024
ce8cfe5
refactor: makeFakeImageFile() 함수 이름 변경
hxeyexn Dec 18, 2024
2ece854
refactor: MockWebServerFixture 이름 변경
hxeyexn Dec 18, 2024
e26952f
refactor: makeMockResponse() 함수 이름 변경
hxeyexn Dec 18, 2024
3b8ff3d
style: ktlintformat
hxeyexn Dec 18, 2024
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
11 changes: 11 additions & 0 deletions android/Staccato_AN/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ plugins {
alias(libs.plugins.firebaseCrashlytics)
alias(libs.plugins.mapsplatformSecretsGradlePlugin)
alias(libs.plugins.hiltAndroid)
alias(libs.plugins.androidJunit5)
}

android {
Expand All @@ -34,6 +35,8 @@ android {
versionName = "1.2.1"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["runnerBuilder"] =
"de.mannodermaus.junit5.AndroidJUnit5Builder"

buildConfigField("String", "TOKEN", "${localProperties["token"]}")
}
Expand Down Expand Up @@ -101,6 +104,10 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

// JUnit5
testImplementation(libs.junit.jupiter)
testRuntimeOnly(libs.junit.jupiter.engine)

// Glide
implementation(libs.glide)

Expand All @@ -117,6 +124,7 @@ dependencies {

// OkHttp
implementation(libs.okhttp.logging.interceptor)
testImplementation(libs.okhttp.mockwebserver)

// Lifecycle
implementation(libs.lifecycle.viewmodel)
Expand All @@ -139,6 +147,9 @@ dependencies {
// Fragment
implementation(libs.androidx.fragment.ktx)

// Coroutines Test
testImplementation(libs.kotlinx.coroutines.test)

// Mockk
testImplementation(libs.mockk.android)
testImplementation(libs.mockk.agent)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.on.staccato.data

import com.on.staccato.data.dto.Status

private const val EXCEPTION_NETWORK_ERROR_MESSAGE = "네트워크 연결이 불안정합니다.\n연결을 재설정한 후 다시 시도해 주세요."

sealed interface ApiResult<T : Any>

class Success<T : Any>(val data: T) : ApiResult<T>

class ServerError<T : Any>(val status: Status, val message: String) : ApiResult<T>

class Exception<T : Any>(val e: Throwable, val message: String = EXCEPTION_NETWORK_ERROR_MESSAGE) : ApiResult<T>

inline fun <T : Any, R : Any> ApiResult<T>.handle(convert: (T) -> R): ApiResult<R> {
return when (this) {
is Exception -> Exception(e)
is ServerError -> ServerError(status, message)
is Success -> Success(convert(data))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.on.staccato.data

import com.on.staccato.data.dto.ErrorResponse
import com.on.staccato.data.dto.Status
import okhttp3.Request
import okhttp3.ResponseBody
import okio.Timeout
import retrofit2.Call
import retrofit2.HttpException
import retrofit2.Response

class ApiResultCall<T : Any>(
private val proxy: Call<T>,
) : Call<ApiResult<T>> {
override fun enqueue(callback: retrofit2.Callback<ApiResult<T>>) {
proxy.enqueue(
object : retrofit2.Callback<T> {
override fun onResponse(
call: Call<T>,
response: Response<T>,
) {
val networkResult = handleApiResponse { response }
callback.onResponse(this@ApiResultCall, Response.success(networkResult))
}

override fun onFailure(
call: Call<T>,
t: Throwable,
) {
val networkResult = Exception<T>(t)
callback.onResponse(this@ApiResultCall, Response.success(networkResult))
}
},
)
}

override fun execute(): Response<ApiResult<T>> = throw NotImplementedError()

override fun clone(): Call<ApiResult<T>> = ApiResultCall(proxy.clone())

override fun isExecuted(): Boolean = proxy.isExecuted

override fun cancel() {
proxy.cancel()
}

override fun isCanceled(): Boolean = proxy.isCanceled

override fun request(): Request = proxy.request()

override fun timeout(): Timeout = proxy.timeout()
}

private const val CREATED = 201
private const val NOT_FOUND_ERROR_BODY = "errorBody를 찾을 수 없습니다."

private fun <T : Any> handleApiResponse(execute: () -> Response<T>): ApiResult<T> {
return try {
val response: Response<T> = execute()
val body: T? = response.body()

when {
response.isSuccessful && response.code() == CREATED -> Success(body as T)
response.isSuccessful && body != null -> Success(body)
else -> {
val errorBody: ResponseBody =
response.errorBody()
?: throw IllegalArgumentException(NOT_FOUND_ERROR_BODY)
val errorResponse: ErrorResponse = StaccatoClient.getErrorResponse(errorBody)
ServerError(
status = Status.Message(errorResponse.status),
message = errorResponse.message,
)
}
}
} catch (e: HttpException) {
ServerError(status = Status.Code(e.code()), message = e.message())
} catch (e: Throwable) {
Exception(e, message = e.message.toString())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.on.staccato.data

import retrofit2.Call
import retrofit2.CallAdapter
import java.lang.reflect.Type

class ApiResultCallAdapter(
private val resultType: Type,
) : CallAdapter<Type, Call<ApiResult<Type>>> {
override fun responseType(): Type = resultType

override fun adapt(call: Call<Type>): Call<ApiResult<Type>> = ApiResultCall(call)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.on.staccato.data

import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

class ApiResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit,
): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java) {
return null
}

val callType = getParameterUpperBound(0, returnType as ParameterizedType)
if (getRawType(callType) != ApiResult::class.java) {
return null
}

val resultType = getParameterUpperBound(0, callType as ParameterizedType)
return ApiResultCallAdapter(resultType)
}

companion object {
fun create(): ApiResultCallAdapterFactory = ApiResultCallAdapterFactory()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.on.staccato.data

import com.on.staccato.data.dto.Status

suspend fun <T : Any> ApiResult<T>.onSuccess(executable: suspend (T) -> Unit): ApiResult<T> =
apply {
if (this is Success<T>) {
executable(data)
}
}

suspend fun <T : Any> ApiResult<T>.onServerError(executable: suspend (status: Status, message: String) -> Unit): ApiResult<T> =
apply {
if (this is ServerError<T>) {
executable(status, message)
}
}

suspend fun <T : Any> ApiResult<T>.onException(executable: suspend (e: Throwable, message: String) -> Unit): ApiResult<T> =
apply {
if (this is Exception<T>) {
executable(e, message)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ object StaccatoClient {
.addConverterFactory(
jsonBuilder.asConverterFactory("application/json".toMediaType()),
)
.addCallAdapterFactory(ApiResultCallAdapterFactory.create())
.build()

fun getErrorResponse(errorBody: ResponseBody): ErrorResponse {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.on.staccato.data.comment

import com.on.staccato.data.ApiResult
import com.on.staccato.data.dto.comment.CommentRequest
import com.on.staccato.data.dto.comment.CommentUpdateRequest
import com.on.staccato.data.dto.comment.CommentsResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
Expand All @@ -16,23 +16,23 @@ interface CommentApiService {
@GET(COMMENTS_URI)
suspend fun getComments(
@Query(STACCATO_ID) staccatoId: Long,
): Response<CommentsResponse>
): ApiResult<CommentsResponse>

@POST(COMMENTS_URI)
suspend fun postComment(
@Body commentRequest: CommentRequest,
): Response<Unit>
): ApiResult<Unit>

@PUT(COMMENTS_URI_WITH_COMMENT_ID)
suspend fun putComment(
@Path(COMMENT_ID) commentId: Long,
@Body commentUpdateRequest: CommentUpdateRequest,
): Response<Unit>
): ApiResult<Unit>

@DELETE(COMMENTS_URI_WITH_COMMENT_ID)
suspend fun deleteComment(
@Path(COMMENT_ID) commentId: Long,
): Response<Unit>
): ApiResult<Unit>

companion object {
private const val COMMENTS_URI = "/comments"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package com.on.staccato.data.comment

import com.on.staccato.data.ResponseResult
import com.on.staccato.data.ApiResult
import com.on.staccato.data.dto.comment.CommentRequest
import com.on.staccato.data.dto.comment.CommentUpdateRequest
import com.on.staccato.data.dto.comment.CommentsResponse

interface CommentDataSource {
suspend fun getComments(staccatoId: Long): ResponseResult<CommentsResponse>
suspend fun getComments(staccatoId: Long): ApiResult<CommentsResponse>

suspend fun createComment(commentRequest: CommentRequest): ResponseResult<Unit>
suspend fun createComment(commentRequest: CommentRequest): ApiResult<Unit>

suspend fun updateComment(
commentId: Long,
commentUpdateRequest: CommentUpdateRequest,
): ResponseResult<Unit>
): ApiResult<Unit>

suspend fun deleteComment(commentId: Long): ResponseResult<Unit>
suspend fun deleteComment(commentId: Long): ApiResult<Unit>
}
Loading
Loading