Skip to content

Commit

Permalink
[Test/#250] Article 목록조회(무한스크롤) Mock API 구현 (#251)
Browse files Browse the repository at this point in the history
* feat: 아티클 목록 조회 목킹 API 프로토타입

* refacotr: ReadArticleResponse 에서 uc out dto 의존성 제거

* test: 아티클 목록 조회 컨트롤러 테스트 작성 (swagger 허브 등록)

* test: 아티클 조회 includedWorkbooks 필드 Null 처리

* test: 아티클 단건 조회에서 includedWorkbooks 필드에 대한 Description 추가

* test: 응답 바디 type descrition 수정

* test: 응답 바디 type descrition 수정 (fieldWithObject -> fieldWithArray)

* test: 아티클 목록 조회 API URI 일부 수정

* test: 아티클 목록 조회 API reponse body description 추가

* test: 학습지 목록 조회 응답 body type 수정

* test: article 목록 조회 API field description 수정

* test: field path 수정 (data.articles -> data.articles[])

* test: 아티클 단건 조회 테스트에서 fieild descritpion 수정
  • Loading branch information
hun-ca authored Jul 25, 2024
1 parent dd1d3ad commit 13d2fb4
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.few.api.domain.article.usecase

import com.few.api.domain.article.usecase.dto.ReadArticlesUseCaseIn
import com.few.api.domain.article.usecase.dto.ReadArticlesUseCaseOut
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

@Component
class ReadArticlesUseCase {

@Transactional(readOnly = true)
fun execute(useCaseIn: ReadArticlesUseCaseIn): ReadArticlesUseCaseOut {
return ReadArticlesUseCaseOut(emptyList()) // TODO: impl
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ data class ReadArticleUseCaseOut(
val category: String,
val createdAt: LocalDateTime,
val views: Long,
val includedWorkbooks: List<WorkbookDetail> = emptyList(),
)

data class WriterDetail(
val id: Long,
val name: String,
val url: URL,
)

data class WorkbookDetail(
val id: Long,
val title: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.domain.article.usecase.dto

data class ReadArticlesUseCaseIn(
val prevArticleId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.domain.article.usecase.dto

data class ReadArticlesUseCaseOut(
val articles: List<ReadArticleUseCaseOut>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,26 @@ package com.few.api.web.controller.article

import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseIn
import com.few.api.domain.article.usecase.ReadArticleUseCase
import com.few.api.domain.article.usecase.ReadArticlesUseCase
import com.few.api.domain.article.usecase.dto.ReadArticlesUseCaseIn
import com.few.api.web.controller.article.response.ReadArticleResponse
import com.few.api.web.controller.article.response.ReadArticlesResponse
import com.few.api.web.controller.article.response.WorkbookInfo
import com.few.api.web.controller.article.response.WriterInfo
import com.few.api.web.support.ApiResponse
import com.few.api.web.support.ApiResponseGenerator
import jakarta.validation.constraints.Min
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*

@Validated
@RestController
@RequestMapping(value = ["/api/v1/articles"], produces = [MediaType.APPLICATION_JSON_VALUE])
class ArticleController(
private val readArticleUseCase: ReadArticleUseCase,
private val readArticlesUseCase: ReadArticlesUseCase,
) {

@GetMapping("/{articleId}")
Expand All @@ -31,8 +34,58 @@ class ArticleController(
readArticleUseCase.execute(useCaseIn)
}

return ReadArticleResponse(useCaseOut).let {
ApiResponseGenerator.success(it, HttpStatus.OK)
}
val response = ReadArticleResponse(
id = useCaseOut.id,
title = useCaseOut.title,
writer = WriterInfo(
useCaseOut.writer.id,
useCaseOut.writer.name,
useCaseOut.writer.url
),
content = useCaseOut.content,
problemIds = useCaseOut.problemIds,
category = useCaseOut.category,
createdAt = useCaseOut.createdAt,
views = useCaseOut.views
)

return ApiResponseGenerator.success(response, HttpStatus.OK)
}

@GetMapping
fun readArticles(
@RequestParam(
required = false,
defaultValue = "0"
) prevArticleId: Long,
): ApiResponse<ApiResponse.SuccessBody<ReadArticlesResponse>> {
val useCaseOut = readArticlesUseCase.execute(ReadArticlesUseCaseIn(prevArticleId))

val articles: List<ReadArticleResponse> = useCaseOut.articles.map { a ->
ReadArticleResponse(
id = a.id,
title = a.title,
writer = WriterInfo(
a.writer.id,
a.writer.name,
a.writer.url
),
content = a.content,
problemIds = a.problemIds,
category = a.category,
createdAt = a.createdAt,
views = a.views,
includedWorkbooks = a.includedWorkbooks.map { w ->
WorkbookInfo(
id = w.id,
title = w.title
)
}
)
}.toList()

val response = ReadArticlesResponse(articles, articles.size != 10) // TODO refactor 'isLast'

return ApiResponseGenerator.success(response, HttpStatus.OK)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.few.api.web.controller.article.response

import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseOut
import java.net.URL
import java.time.LocalDateTime

Expand All @@ -13,27 +12,16 @@ data class ReadArticleResponse(
val category: String,
val createdAt: LocalDateTime,
val views: Long,
) {
constructor(
useCaseOut: ReadArticleUseCaseOut,
) : this(
id = useCaseOut.id,
writer = WriterInfo(
id = useCaseOut.writer.id,
name = useCaseOut.writer.name,
url = useCaseOut.writer.url
),
title = useCaseOut.title,
content = useCaseOut.content,
problemIds = useCaseOut.problemIds,
category = useCaseOut.category,
createdAt = useCaseOut.createdAt,
views = useCaseOut.views
)
}
val includedWorkbooks: List<WorkbookInfo> = emptyList(),
)

data class WriterInfo(
val id: Long,
val name: String,
val url: URL,
)

data class WorkbookInfo(
val id: Long,
val title: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.few.api.web.controller.article.response

data class ReadArticlesResponse(
val articles: List<ReadArticleResponse>,
val isLast: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName
import com.epages.restdocs.apispec.ResourceSnippetParameters
import com.epages.restdocs.apispec.Schema
import com.fasterxml.jackson.databind.ObjectMapper
import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseIn
import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseOut
import com.few.api.domain.article.usecase.dto.WriterDetail
import com.few.api.domain.article.usecase.ReadArticleUseCase
import com.few.api.domain.article.usecase.ReadArticlesUseCase
import com.few.api.domain.article.usecase.dto.*
import com.few.api.web.controller.ControllerTestSpec
import com.few.api.web.controller.description.Description
import com.few.api.web.controller.helper.*
Expand Down Expand Up @@ -43,6 +42,9 @@ class ArticleControllerTest : ControllerTestSpec() {
@MockBean
private lateinit var readArticleUseCase: ReadArticleUseCase

@MockBean
private lateinit var readArticlesUseCase: ReadArticlesUseCase

companion object {
private val BASE_URL = "/api/v1/articles"
private val TAG = "ArticleController"
Expand Down Expand Up @@ -96,7 +98,7 @@ class ArticleControllerTest : ControllerTestSpec() {
ResourceSnippetParameters.builder().description("아티클 Id로 아티클 조회")
.summary(api.toIdentifier()).privateResource(false).deprecated(false)
.tag(TAG).requestSchema(Schema.schema(api.toRequestSchema()))
.pathParameters(parameterWithName("articleId").description("학습지 Id"))
.pathParameters(parameterWithName("articleId").description("아티클 Id"))
.responseSchema(Schema.schema(api.toResponseSchema())).responseFields(
*Description.describe(
arrayOf(
Expand All @@ -123,7 +125,101 @@ class ArticleControllerTest : ControllerTestSpec() {
PayloadDocumentation.fieldWithPath("data.createdAt")
.fieldWithString("아티클 생성일"),
PayloadDocumentation.fieldWithPath("data.views")
.fieldWithNumber("아티클 조회수")
.fieldWithNumber("아티클 조회수"),
PayloadDocumentation.fieldWithPath("data.includedWorkbooks")
.fieldWithArray("아티클이 포함된 학습지 정보(해당 API에서 사용되지 않음)")
)
)
).build()
)
)
)
}

@Test
@DisplayName("[GET] /api/v1/articles?prevArticleId={prevArticleId}")
fun readArticles() {
// given
val api = "ReadArticles"
val uri = UriComponentsBuilder.newInstance()
.path("$BASE_URL")
.queryParam("prevArticleId", 1L)
.build()
.toUriString()
// set usecase mock
val prevArticleId = 1L
`when`(readArticlesUseCase.execute(ReadArticlesUseCaseIn(prevArticleId))).thenReturn(
ReadArticlesUseCaseOut(
listOf(
ReadArticleUseCaseOut(
id = 1L,
writer = WriterDetail(
id = 1L,
name = "안나포",
url = URL("http://localhost:8080/api/v1/writers/1")
),
title = "ETF(상장 지수 펀드)란? 모르면 손해라고?",
content = CategoryType.fromCode(0)!!.name,
problemIds = listOf(1L, 2L, 3L),
category = "경제",
createdAt = LocalDateTime.now(),
views = 1L,
includedWorkbooks = listOf(
WorkbookDetail(1L, "사소한 것들의 역사"),
WorkbookDetail(2L, "인모스트 경제레터")
)
)
)
)
)

// when
this.webTestClient.get().uri(uri).accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody().consumeWith(
WebTestClientRestDocumentation.document(
api.toIdentifier(),
ResourceDocumentation.resource(
ResourceSnippetParameters.builder().description("아티 목록 10개씩 조회(조회수 기반 정렬)")
.summary(api.toIdentifier()).privateResource(false).deprecated(false)
.tag(TAG).requestSchema(Schema.schema(api.toRequestSchema()))
.queryParameters(parameterWithName("prevArticleId").description("이전까지 조회한 아티클 Id"))
.responseSchema(Schema.schema(api.toResponseSchema())).responseFields(
*Description.describe(
arrayOf(
PayloadDocumentation.fieldWithPath("data")
.fieldWithObject("data"),
PayloadDocumentation.fieldWithPath("data.isLast")
.fieldWithBoolean("마지막 스크롤 유무"),
PayloadDocumentation.fieldWithPath("data.articles")
.fieldWithArray("아티클 목록"),
PayloadDocumentation.fieldWithPath("data.articles[].id")
.fieldWithNumber("아티클 Id"),
PayloadDocumentation.fieldWithPath("data.articles[].writer")
.fieldWithObject("아티클 작가"),
PayloadDocumentation.fieldWithPath("data.articles[].writer.id")
.fieldWithNumber("아티클 작가 Id"),
PayloadDocumentation.fieldWithPath("data.articles[].writer.name")
.fieldWithString("아티클 작가 이름"),
PayloadDocumentation.fieldWithPath("data.articles[].writer.url")
.fieldWithString("아티클 작가 링크"),
PayloadDocumentation.fieldWithPath("data.articles[].title")
.fieldWithString("아티클 제목"),
PayloadDocumentation.fieldWithPath("data.articles[].content")
.fieldWithString("아티클 내용"),
PayloadDocumentation.fieldWithPath("data.articles[].problemIds")
.fieldWithArray("아티클 문제 목록"),
PayloadDocumentation.fieldWithPath("data.articles[].category")
.fieldWithString("아티클 카테고리"),
PayloadDocumentation.fieldWithPath("data.articles[].createdAt")
.fieldWithString("아티클 생성일"),
PayloadDocumentation.fieldWithPath("data.articles[].views")
.fieldWithNumber("아티클 조회수"),
PayloadDocumentation.fieldWithPath("data.articles[].includedWorkbooks")
.fieldWithArray("아티클이 포함된 학습지 정보"),
PayloadDocumentation.fieldWithPath("data.articles[].includedWorkbooks[].id")
.fieldWithNumber("아티클이 포함된 학습지 정보(학습지ID)"),
PayloadDocumentation.fieldWithPath("data.articles[].includedWorkbooks[].title")
.fieldWithString("아티클이 포함된 학습지 정보(학습지 제목)")
)
)
).build()
Expand Down

0 comments on commit 13d2fb4

Please sign in to comment.