test: ReservationTimeController의 테스트 코드 코틀린 전환

This commit is contained in:
이상진 2025-07-18 18:32:55 +09:00
parent b15b8b7ae8
commit 5c6c52cf41

View File

@ -1,254 +1,306 @@
package roomescape.reservation.web; package roomescape.reservation.web
import static org.hamcrest.Matchers.*; import com.ninjasquad.springmockk.MockkBean
import com.ninjasquad.springmockk.SpykBean
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import io.mockk.every
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.context.annotation.Import
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import roomescape.common.config.JacksonConfig
import roomescape.common.exception.ErrorType
import roomescape.reservation.business.ReservationTimeService
import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
import roomescape.reservation.infrastructure.persistence.ReservationTimeRepository
import roomescape.util.ReservationFixture
import roomescape.util.ReservationTimeFixture
import roomescape.util.RoomescapeApiTest
import roomescape.util.ThemeFixture
import java.time.LocalDate
import java.time.LocalTime
import java.time.LocalDate; @WebMvcTest(ReservationTimeController::class)
import java.time.LocalTime; @Import(JacksonConfig::class)
import java.util.Map; class ReservationTimeControllerTest(
import java.util.stream.Stream; val mockMvc: MockMvc,
) : RoomescapeApiTest() {
import org.junit.jupiter.api.DisplayName; @SpykBean
import org.junit.jupiter.api.Test; private lateinit var reservationTimeService: ReservationTimeService
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
import io.restassured.RestAssured; @MockkBean
import io.restassured.http.ContentType; private lateinit var reservationTimeRepository: ReservationTimeRepository
import io.restassured.http.Header;
import roomescape.member.infrastructure.persistence.MemberEntity;
import roomescape.member.infrastructure.persistence.MemberRepository;
import roomescape.member.infrastructure.persistence.Role;
import roomescape.reservation.infrastructure.persistence.ReservationEntity;
import roomescape.reservation.infrastructure.persistence.ReservationRepository;
import roomescape.reservation.infrastructure.persistence.ReservationStatus;
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity;
import roomescape.reservation.infrastructure.persistence.ReservationTimeRepository;
import roomescape.theme.infrastructure.persistence.ThemeEntity;
import roomescape.theme.infrastructure.persistence.ThemeRepository;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @MockkBean
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) private lateinit var reservationRepository: ReservationRepository
public class ReservationTimeControllerTest {
@Autowired init {
private ReservationTimeRepository reservationTimeRepository; Given("등록된 모든 시간을 조회할 때") {
val endpoint = "/times"
@Autowired When("관리자인 경우") {
private ThemeRepository themeRepository; beforeTest {
loginAsAdmin()
@Autowired
private ReservationRepository reservationRepository;
@Autowired
private MemberRepository memberRepository;
@LocalServerPort
private int port;
private final Map<String, String> params = Map.of(
"startAt", "17:00"
);
@Test
@DisplayName("처음으로 등록하는 시간의 id는 1이다.")
void firstPost() {
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password");
RestAssured.given().log().all()
.contentType(ContentType.JSON)
.port(port)
.header(new Header("Cookie", adminAccessTokenCookie))
.body(params)
.when().post("/times")
.then().log().all()
.statusCode(201)
.body("data.id", is(1))
.header("Location", "/times/1");
} }
@Test Then("정상 응답") {
@DisplayName("아무 시간도 등록 하지 않은 경우, 시간 목록 조회 결과 개수는 0개이다.") every {
void readEmptyTimes() { reservationTimeRepository.findAll()
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password"); } returns listOf(
ReservationTimeFixture.create(id = 1L),
RestAssured.given().log().all() ReservationTimeFixture.create(id = 2L)
.port(port)
.header(new Header("Cookie", adminAccessTokenCookie))
.when().get("/times")
.then().log().all()
.statusCode(200)
.body("data.times.size()", is(0));
}
@Test
@DisplayName("하나의 시간만 등록한 경우, 시간 목록 조회 결과 개수는 1개이다.")
void readTimesSizeAfterFirstPost() {
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password");
RestAssured.given().log().all()
.contentType(ContentType.JSON)
.port(port)
.header(new Header("Cookie", adminAccessTokenCookie))
.body(params)
.when().post("/times")
.then().log().all()
.statusCode(201)
.body("data.id", is(1))
.header("Location", "/times/1");
RestAssured.given().log().all()
.port(port)
.header(new Header("Cookie", adminAccessTokenCookie))
.when().get("/times")
.then().log().all()
.statusCode(200)
.body("data.times.size()", is(1));
}
@Test
@DisplayName("하나의 시간만 등록한 경우, 시간 삭제 뒤 시간 목록 조회 결과 개수는 0개이다.")
void readTimesSizeAfterPostAndDelete() {
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password");
RestAssured.given().log().all()
.contentType(ContentType.JSON)
.port(port)
.header(new Header("Cookie", adminAccessTokenCookie))
.body(params)
.when().post("/times")
.then().log().all()
.statusCode(201)
.body("data.id", is(1))
.header("Location", "/times/1");
RestAssured.given().log().all()
.port(port)
.header(new Header("Cookie", adminAccessTokenCookie))
.when().delete("/times/1")
.then().log().all()
.statusCode(204);
RestAssured.given().log().all()
.port(port)
.header(new Header("Cookie", adminAccessTokenCookie))
.when().get("/times")
.then().log().all()
.statusCode(200)
.body("data.times.size()", is(0));
}
@ParameterizedTest
@MethodSource("validateRequestDataFormatSource")
@DisplayName("예약 시간 생성 시, 시간 요청 데이터에 시간 포맷이 아닌 값이 입력되어오면 400 에러를 발생한다.")
void validateRequestDataFormat(Map<String, String> request) {
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password");
RestAssured.given().log().all()
.contentType(ContentType.JSON)
.header(new Header("Cookie", adminAccessTokenCookie))
.port(port)
.body(request)
.when().post("/times")
.then().log().all()
.statusCode(400);
}
static Stream<Map<String, String>> validateRequestDataFormatSource() {
return Stream.of(
Map.of(
"startAt", "24:59"
),
Map.of(
"startAt", "hihi")
);
}
@ParameterizedTest
@MethodSource("validateBlankRequestSource")
@DisplayName("예약 시간 생성 시, 요청 값에 공백 또는 null이 포함되어 있으면 400 에러를 발생한다.")
void validateBlankRequest(Map<String, String> request) {
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password");
RestAssured.given().log().all()
.contentType(ContentType.JSON)
.header(new Header("Cookie", adminAccessTokenCookie))
.port(port)
.body(request)
.when().post("/times")
.then().log().all()
.statusCode(400);
}
static Stream<Map<String, String>> validateBlankRequestSource() {
return Stream.of(
Map.of(
),
Map.of(
"startAt", ""
),
Map.of(
"startAt", " "
) )
);
}
private String getAdminAccessTokenCookieByLogin(String email, String password) { runGetTest(
memberRepository.save(new MemberEntity(null, "이름", email, password, Role.ADMIN)); mockMvc = mockMvc,
endpoint = endpoint,
Map<String, String> loginParams = Map.of( log = true
"email", email, ) {
"password", password status { isOk() }
); content {
contentType(MediaType.APPLICATION_JSON)
String accessToken = RestAssured.given().log().all() jsonPath("$.data.times[0].id") { value(1) }
.contentType(ContentType.JSON) jsonPath("$.data.times[1].id") { value(2) }
.port(port) }
.body(loginParams) }
.when().post("/login") }
.then().log().all().extract().cookie("accessToken"); }
return "accessToken=" + accessToken; When("관리자가 아닌 경우") {
} loginAsUser()
@Test Then("로그인 페이지로 이동") {
@DisplayName("특정 날짜의 특정 테마 예약 현황을 조회한다.") runGetTest(
void readReservationByDateAndThemeId() { mockMvc = mockMvc,
// given endpoint = endpoint,
LocalDate today = LocalDate.now(); log = true
ReservationTimeEntity reservationTime1 = reservationTimeRepository.save( ) {
new ReservationTimeEntity(null, LocalTime.of(17, 0))); status { is3xxRedirection() }
ReservationTimeEntity reservationTime2 = reservationTimeRepository.save( header { string("Location", "/login") }
new ReservationTimeEntity(null, LocalTime.of(17, 30))); }
ReservationTimeEntity reservationTime3 = reservationTimeRepository.save( }
new ReservationTimeEntity(null, LocalTime.of(18, 30))); }
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명1", "설명", "썸네일URL")); }
MemberEntity member = memberRepository.save(
new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)); Given("시간을 추가할 때") {
val endpoint = "/times"
reservationRepository.save(
new ReservationEntity(null, today.plusDays(1), reservationTime1, theme, member, When("관리자인 경우") {
ReservationStatus.CONFIRMED)); beforeTest {
reservationRepository.save( loginAsAdmin()
new ReservationEntity(null, today.plusDays(1), reservationTime2, theme, member, }
ReservationStatus.CONFIRMED)); val time = LocalTime.of(10, 0)
reservationRepository.save( val request = ReservationTimeRequest(startAt = time)
new ReservationEntity(null, today.plusDays(1), reservationTime3, theme, member,
ReservationStatus.CONFIRMED)); Then("시간 형식이 HH:mm이 아니거나, 범위를 벗어나면 400 응답") {
listOf(
// when & then "{\"startAt\": \"23:30:30\"}",
RestAssured.given().log().all() "{\"startAt\": \"24:59\"}",
.contentType(ContentType.JSON) ).forEach {
.port(port) runPostTest(
.header("Cookie", getAdminAccessTokenCookieByLogin("a@a.a", "a")) mockMvc = mockMvc,
.when().get("/times/filter?date={date}&themeId={themeId}", today.plusDays(1).toString(), theme.getId()) endpoint = endpoint,
.then().log().all() body = it,
.statusCode(200) log = true
.body("data.reservationTimes.size()", is(3)); ) {
status { isBadRequest() }
}
}
}
Then("정상 응답") {
every {
reservationTimeService.addTime(request)
} returns ReservationTimeResponse(id = 1, startAt = time)
runPostTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = request,
log = true
) {
status { isCreated() }
content {
contentType(MediaType.APPLICATION_JSON)
jsonPath("$.data.id") { value(1) }
jsonPath("$.data.startAt") { value("10:00") }
}
}
}
Then("동일한 시간이 존재하면 409 응답") {
every {
reservationTimeRepository.existsByStartAt(time)
} returns true
runPostTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = request,
log = true
) {
status { isConflict() }
content {
contentType(MediaType.APPLICATION_JSON)
jsonPath("$.errorType") { value(ErrorType.TIME_DUPLICATED.name) }
}
}
}
}
When("관리자가 아닌 경우") {
loginAsUser()
Then("로그인 페이지로 이동") {
runPostTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = ReservationTimeFixture.create(),
log = true
) {
status { is3xxRedirection() }
header { string("Location", "/login") }
}
}
}
}
Given("시간을 삭제할 때") {
val endpoint = "/times/1"
When("관리자인 경우") {
beforeTest {
loginAsAdmin()
}
Then("정상 응답") {
every {
reservationTimeService.removeTimeById(1L)
} returns Unit
runDeleteTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { isNoContent() }
}
}
Then("없는 시간을 조회하면 400 응답") {
val id = 1L
every {
reservationTimeRepository.findByIdOrNull(id)
} returns null
runDeleteTest(
mockMvc = mockMvc,
endpoint = "/times/$id",
log = true
) {
status { isBadRequest() }
content {
contentType(MediaType.APPLICATION_JSON)
jsonPath("$.errorType") { value(ErrorType.RESERVATION_TIME_NOT_FOUND.name) }
}
}
}
Then("예약이 있는 시간을 삭제하면 409 응답") {
val id = 1L
every {
reservationTimeRepository.findByIdOrNull(id)
} returns ReservationTimeFixture.create(id = id)
every {
reservationRepository.findByReservationTime(any())
} returns listOf(ReservationFixture.create())
runDeleteTest(
mockMvc = mockMvc,
endpoint = "/times/$id",
log = true
) {
status { isConflict() }
content {
contentType(MediaType.APPLICATION_JSON)
jsonPath("$.errorType") { value(ErrorType.TIME_IS_USED_CONFLICT.name) }
}
}
}
}
When("관리자가 아닌 경우") {
loginAsUser()
Then("로그인 페이지로 이동") {
runDeleteTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { is3xxRedirection() }
header { string("Location", "/login") }
}
}
}
}
Given("날짜, 테마가 주어졌을 때") {
loginAsUser()
val date: LocalDate = LocalDate.now()
val themeId = 1L
When("저장된 예약 시간이 있으면") {
val times: List<ReservationTimeEntity> = listOf(
ReservationTimeFixture.create(id = 1L, startAt = LocalTime.of(10, 0)),
ReservationTimeFixture.create(id = 2L, startAt = LocalTime.of(11, 0))
)
every {
reservationTimeRepository.findAll()
} returns times
Then("그 시간과, 해당 날짜와 테마에 대한 예약 여부가 담긴 목록을 응답") {
every {
reservationRepository.findByDateAndThemeId(date, themeId)
} returns listOf(
ReservationFixture.create(
id = 1L,
date = date,
theme = ThemeFixture.create(id = themeId),
reservationTime = times[0]
)
)
val response = runGetTest(
mockMvc = mockMvc,
endpoint = "/times/filter?date=$date&themeId=$themeId",
log = true
) {
status { isOk() }
content {
contentType(MediaType.APPLICATION_JSON)
}
}.andReturn().readValue(ReservationTimeInfosResponse::class.java)
assertSoftly(response.times) {
this shouldHaveSize times.size
this[0].id shouldBe times[0].id
this[0].alreadyBooked shouldBe true
this[1].id shouldBe times[1].id
this[1].alreadyBooked shouldBe false
}
}
}
}
} }
} }