From 612bbfbddc2a3780a3ecb1be2985bacb3528681b Mon Sep 17 00:00:00 2001 From: pricelees Date: Sat, 2 Aug 2025 15:56:31 +0900 Subject: [PATCH] =?UTF-8?q?test:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EC=82=AC=ED=95=AD=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/business/AuthServiceTest.kt | 3 +- .../payment/business/PaymentServiceTest.kt | 3 +- .../client/TossPaymentClientTest.kt | 3 + .../CanceledPaymentRepositoryTest.kt | 6 +- .../persistence/PaymentRepositoryTest.kt | 8 +- .../business/ReservationServiceTest.kt | 2 + .../persistence/ReservationRepositoryTest.kt | 2 +- .../ReservationSearchSpecificationTest.kt | 2 +- .../web/ReservationControllerTest.kt | 471 ++++++++---------- .../theme/business/ThemeServiceTest.kt | 3 +- .../persistence/ThemeRepositoryTest.kt | 2 +- .../theme/web/ThemeControllerTest.kt | 1 + .../time/business/TimeServiceTest.kt | 2 + .../persistence/TimeRepositoryTest.kt | 2 +- .../roomescape/time/web/TimeControllerTest.kt | 2 +- src/test/kotlin/roomescape/util/Fixtures.kt | 21 +- .../roomescape/util/RoomescapeApiTest.kt | 15 + 17 files changed, 259 insertions(+), 289 deletions(-) diff --git a/src/test/kotlin/roomescape/auth/business/AuthServiceTest.kt b/src/test/kotlin/roomescape/auth/business/AuthServiceTest.kt index 8d629510..aa570047 100644 --- a/src/test/kotlin/roomescape/auth/business/AuthServiceTest.kt +++ b/src/test/kotlin/roomescape/auth/business/AuthServiceTest.kt @@ -15,10 +15,11 @@ import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.MemberRepository import roomescape.util.JwtFixture import roomescape.util.MemberFixture +import roomescape.util.TsidFactory class AuthServiceTest : BehaviorSpec({ val memberRepository: MemberRepository = mockk() - val memberService: MemberService = MemberService(memberRepository) + val memberService = MemberService(TsidFactory, memberRepository) val jwtHandler: JwtHandler = JwtFixture.create() val authService = AuthService(memberService, jwtHandler) diff --git a/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt b/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt index 7b5f2e36..71d9696d 100644 --- a/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt +++ b/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt @@ -14,13 +14,14 @@ import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository import roomescape.payment.infrastructure.persistence.PaymentRepository import roomescape.payment.web.PaymentCancelRequest import roomescape.util.PaymentFixture +import roomescape.util.TsidFactory import java.time.OffsetDateTime class PaymentServiceTest : FunSpec({ val paymentRepository: PaymentRepository = mockk() val canceledPaymentRepository: CanceledPaymentRepository = mockk() - val paymentService = PaymentService(paymentRepository, canceledPaymentRepository) + val paymentService = PaymentService(TsidFactory, paymentRepository, canceledPaymentRepository) context("createCanceledPaymentByReservationId") { val reservationId = 1L diff --git a/src/test/kotlin/roomescape/payment/infrastructure/client/TossPaymentClientTest.kt b/src/test/kotlin/roomescape/payment/infrastructure/client/TossPaymentClientTest.kt index f5984ddd..eb8906ad 100644 --- a/src/test/kotlin/roomescape/payment/infrastructure/client/TossPaymentClientTest.kt +++ b/src/test/kotlin/roomescape/payment/infrastructure/client/TossPaymentClientTest.kt @@ -1,11 +1,13 @@ package roomescape.payment.infrastructure.client +import com.ninjasquad.springmockk.MockkBean import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.client.RestClientTest +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.http.MediaType @@ -20,6 +22,7 @@ import roomescape.payment.web.PaymentCancelRequest import roomescape.payment.web.PaymentCancelResponse @RestClientTest(TossPaymentClient::class) +@MockkBean(JpaMetamodelMappingContext::class) class TossPaymentClientTest( @Autowired val client: TossPaymentClient, @Autowired val mockServer: MockRestServiceServer diff --git a/src/test/kotlin/roomescape/payment/infrastructure/persistence/CanceledPaymentRepositoryTest.kt b/src/test/kotlin/roomescape/payment/infrastructure/persistence/CanceledPaymentRepositoryTest.kt index f08657bb..052d1eb8 100644 --- a/src/test/kotlin/roomescape/payment/infrastructure/persistence/CanceledPaymentRepositoryTest.kt +++ b/src/test/kotlin/roomescape/payment/infrastructure/persistence/CanceledPaymentRepositoryTest.kt @@ -5,10 +5,12 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import roomescape.common.config.next import roomescape.util.PaymentFixture +import roomescape.util.TsidFactory import java.util.* -@DataJpaTest +@DataJpaTest(showSql = false) class CanceledPaymentRepositoryTest( @Autowired val canceledPaymentRepository: CanceledPaymentRepository, ) : FunSpec() { @@ -16,7 +18,7 @@ class CanceledPaymentRepositoryTest( context("paymentKey로 CanceledPaymentEntity 조회") { val paymentKey = "test-payment-key" beforeTest { - PaymentFixture.createCanceled(paymentKey = paymentKey) + PaymentFixture.createCanceled(id = TsidFactory.next(), paymentKey = paymentKey) .also { canceledPaymentRepository.save(it) } } diff --git a/src/test/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt b/src/test/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt index 02e5c165..51979b53 100644 --- a/src/test/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt +++ b/src/test/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt @@ -6,11 +6,13 @@ import io.kotest.matchers.shouldBe import jakarta.persistence.EntityManager import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import roomescape.common.config.next import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.util.PaymentFixture import roomescape.util.ReservationFixture +import roomescape.util.TsidFactory -@DataJpaTest +@DataJpaTest(showSql = false) class PaymentRepositoryTest( @Autowired val paymentRepository: PaymentRepository, @Autowired val entityManager: EntityManager @@ -91,7 +93,9 @@ class PaymentRepositoryTest( } private fun setupReservation(): ReservationEntity { - return ReservationFixture.create().also { + return ReservationFixture.create( + id = TsidFactory.next() + ).also { entityManager.persist(it.member) entityManager.persist(it.theme) entityManager.persist(it.time) diff --git a/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt b/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt index 95f5b118..54c6cccf 100644 --- a/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt +++ b/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt @@ -16,6 +16,7 @@ import roomescape.theme.business.ThemeService import roomescape.time.business.TimeService import roomescape.util.MemberFixture import roomescape.util.ReservationFixture +import roomescape.util.TsidFactory import roomescape.util.TimeFixture import java.time.LocalDate import java.time.LocalTime @@ -27,6 +28,7 @@ class ReservationServiceTest : FunSpec({ val memberService: MemberService = mockk() val themeService: ThemeService = mockk() val reservationService = ReservationService( + TsidFactory, reservationRepository, timeService, memberService, diff --git a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt b/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt index 055ed019..78d44d3f 100644 --- a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt +++ b/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt @@ -15,7 +15,7 @@ import roomescape.util.ReservationFixture import roomescape.util.ThemeFixture import roomescape.util.TimeFixture -@DataJpaTest +@DataJpaTest(showSql = false) class ReservationRepositoryTest( val entityManager: EntityManager, val reservationRepository: ReservationRepository, diff --git a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecificationTest.kt b/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecificationTest.kt index 130b062b..88323f85 100644 --- a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecificationTest.kt +++ b/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecificationTest.kt @@ -15,7 +15,7 @@ import roomescape.util.ThemeFixture import roomescape.util.TimeFixture import java.time.LocalDate -@DataJpaTest +@DataJpaTest(showSql = false) class ReservationSearchSpecificationTest( val entityManager: EntityManager, val reservationRepository: ReservationRepository diff --git a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt index 0ebaad3b..a5fdf245 100644 --- a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt +++ b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt @@ -39,7 +39,7 @@ import java.time.LocalTime class ReservationControllerTest( @LocalServerPort val port: Int, val entityManager: EntityManager, - val transactionTemplate: TransactionTemplate + val transactionTemplate: TransactionTemplate, ) : FunSpec({ extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_EACH_TEST)) }) { @@ -55,24 +55,34 @@ class ReservationControllerTest( @MockkBean lateinit var jwtHandler: JwtHandler + lateinit var testDataHelper: TestDataHelper + + fun login(member: MemberEntity) { + every { jwtHandler.getMemberIdFromToken(any()) } returns member.id!! + every { memberService.findById(member.id!!) } returns member + every { memberIdResolver.resolveArgument(any(), any(), any(), any()) } returns member.id!! + } + init { + beforeSpec { + testDataHelper = TestDataHelper(entityManager, transactionTemplate) + } + context("POST /reservations") { - lateinit var member: MemberEntity beforeTest { - member = login(MemberFixture.create(role = Role.MEMBER)) + val member = testDataHelper.createMember(role = Role.MEMBER) + login(member) } test("정상 응답") { - val reservationRequest = createRequest() + val reservationRequest = testDataHelper.createReservationRequest() val paymentApproveResponse = PaymentFixture.createApproveResponse().copy( paymentKey = reservationRequest.paymentKey, orderId = reservationRequest.orderId, totalAmount = reservationRequest.amount, ) - every { - paymentClient.confirm(any()) - } returns paymentApproveResponse + every { paymentClient.confirm(any()) } returns paymentApproveResponse Given { port(port) @@ -88,12 +98,10 @@ class ReservationControllerTest( } test("결제 과정에서 발생하는 에러는 그대로 응답") { - val reservationRequest = createRequest() + val reservationRequest = testDataHelper.createReservationRequest() val paymentException = PaymentException(PaymentErrorCode.PAYMENT_PROVIDER_ERROR) - every { - paymentClient.confirm(any()) - } throws paymentException + every { paymentClient.confirm(any()) } throws paymentException Given { port(port) @@ -108,24 +116,20 @@ class ReservationControllerTest( } test("결제 완료 후 예약 / 결제 정보 저장 과정에서 에러 발생시 결제 취소 후 에러 응답을 받는다.") { - val reservationRequest = createRequest() + val reservationRequest = testDataHelper.createReservationRequest() val paymentApproveResponse = PaymentFixture.createApproveResponse().copy( paymentKey = reservationRequest.paymentKey, orderId = reservationRequest.orderId, totalAmount = reservationRequest.amount, ) - every { - paymentClient.confirm(any()) - } returns paymentApproveResponse + every { paymentClient.confirm(any()) } returns paymentApproveResponse // 예약 저장 과정에서 테마가 없는 예외 val invalidRequest = reservationRequest.copy(themeId = reservationRequest.themeId + 1) val expectedException = ThemeErrorCode.THEME_NOT_FOUND - every { - paymentClient.cancel(any()) - } returns PaymentFixture.createCancelResponse() + every { paymentClient.cancel(any()) } returns PaymentFixture.createCancelResponse() val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery( "SELECT COUNT(c) FROM CanceledPaymentEntity c", @@ -153,13 +157,13 @@ class ReservationControllerTest( } context("GET /reservations") { - lateinit var reservations: MutableMap> + lateinit var reservations: Map> beforeTest { - reservations = createDummyReservations() + reservations = testDataHelper.createDummyReservations() } test("관리자이면 정상 응답") { - login(MemberFixture.create(role = Role.ADMIN)) + login(testDataHelper.createMember(role = Role.ADMIN)) Given { port(port) contentType(MediaType.APPLICATION_JSON_VALUE) @@ -173,13 +177,14 @@ class ReservationControllerTest( } context("GET /reservations-mine") { - lateinit var reservations: MutableMap> + lateinit var reservations: Map> beforeTest { - reservations = createDummyReservations() + reservations = testDataHelper.createDummyReservations() } test("로그인한 회원이 자신의 예약 목록을 조회한다.") { - val member: MemberEntity = login(reservations.keys.first()) + val member = reservations.keys.first() + login(member) val expectedReservations: Int = reservations[member]?.size ?: 0 Given { @@ -195,9 +200,9 @@ class ReservationControllerTest( } context("GET /reservations/search") { - lateinit var reservations: MutableMap> + lateinit var reservations: Map> beforeTest { - reservations = createDummyReservations() + reservations = testDataHelper.createDummyReservations() } test("관리자만 검색할 수 있다.") { @@ -216,7 +221,7 @@ class ReservationControllerTest( } test("파라미터를 지정하지 않으면 전체 목록 응답") { - login(MemberFixture.create(role = Role.ADMIN)) + login(testDataHelper.createMember(role = Role.ADMIN)) Given { port(port) @@ -230,7 +235,7 @@ class ReservationControllerTest( } test("시작 날짜가 종료 날짜 이전이면 예외 응답") { - login(MemberFixture.create(role = Role.ADMIN)) + login(testDataHelper.createMember(role = Role.ADMIN)) val startDate = LocalDate.now().plusDays(1) val endDate = LocalDate.now() @@ -250,8 +255,8 @@ class ReservationControllerTest( } test("동일한 회원의 모든 예약 응답") { - login(MemberFixture.create(role = Role.ADMIN)) - val member: MemberEntity = reservations.keys.first() + login(testDataHelper.createMember(role = Role.ADMIN)) + val member = reservations.keys.first() Given { port(port) @@ -266,7 +271,7 @@ class ReservationControllerTest( } test("동일한 테마의 모든 예약 응답") { - login(MemberFixture.create(role = Role.ADMIN)) + login(testDataHelper.createMember(role = Role.ADMIN)) val themes = reservations.values.flatten().map { it.theme } val requestThemeId: Long = themes.first().id!! @@ -278,12 +283,12 @@ class ReservationControllerTest( get("/reservations/search") }.Then { statusCode(200) - body("data.reservations.size()", equalTo(themes.filter { it.id == requestThemeId }.size)) + body("data.reservations.size()", equalTo(themes.count { it.id == requestThemeId })) } } test("시작 날짜와 종료 날짜 사이의 예약 응답") { - login(MemberFixture.create(role = Role.ADMIN)) + login(testDataHelper.createMember(role = Role.ADMIN)) val dateFrom: LocalDate = reservations.values.flatten().minOf { it.date } val dateTo: LocalDate = reservations.values.flatten().maxOf { it.date } @@ -302,14 +307,14 @@ class ReservationControllerTest( } context("DELETE /reservations/{id}") { - lateinit var reservations: MutableMap> + lateinit var reservations: Map> beforeTest { - reservations = createDummyReservations() + reservations = testDataHelper.createDummyReservations() } test("관리자만 예약을 삭제할 수 있다.") { - login(MemberFixture.create(role = Role.MEMBER)) - val reservation: ReservationEntity = reservations.values.flatten().first() + login(testDataHelper.createMember(role = Role.MEMBER)) + val reservation = reservations.values.flatten().first() val expectedError = AuthErrorCode.ACCESS_DENIED Given { @@ -323,18 +328,12 @@ class ReservationControllerTest( } test("결제되지 않은 예약은 바로 제거") { - login(MemberFixture.create(role = Role.ADMIN)) - val reservationId: Long = reservations.values.flatten().first().id!! + login(testDataHelper.createMember(role = Role.ADMIN)) + val reservationId = reservations.values.flatten().first().id!! - transactionTemplate.execute { - val reservation: ReservationEntity = entityManager.find( - ReservationEntity::class.java, - reservationId - ) + transactionTemplate.executeWithoutResult { + val reservation = entityManager.find(ReservationEntity::class.java, reservationId) reservation.status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED - entityManager.persist(reservation) - entityManager.flush() - entityManager.clear() } Given { @@ -345,32 +344,18 @@ class ReservationControllerTest( statusCode(HttpStatus.NO_CONTENT.value()) } - // 예약이 삭제되었는지 확인 - transactionTemplate.executeWithoutResult { - val deletedReservation = entityManager.find( - ReservationEntity::class.java, - reservationId - ) - deletedReservation shouldBe null + val deletedReservation = transactionTemplate.execute { + entityManager.find(ReservationEntity::class.java, reservationId) } + deletedReservation shouldBe null } test("결제된 예약은 취소 후 제거") { - login(MemberFixture.create(role = Role.ADMIN)) - val reservation: ReservationEntity = reservations.values.flatten().first() - lateinit var payment: PaymentEntity + login(testDataHelper.createMember(role = Role.ADMIN)) + val reservation = reservations.values.flatten().first { it.status == ReservationStatus.CONFIRMED } + testDataHelper.createPayment(reservation) - transactionTemplate.execute { - payment = PaymentFixture.create(reservation = reservation).also { - entityManager.persist(it) - entityManager.flush() - entityManager.clear() - } - } - - every { - paymentClient.cancel(any()) - } returns PaymentFixture.createCancelResponse() + every { paymentClient.cancel(any()) } returns PaymentFixture.createCancelResponse() val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery( "SELECT COUNT(c) FROM CanceledPaymentEntity c", @@ -396,15 +381,17 @@ class ReservationControllerTest( context("POST /reservations/admin") { test("관리자가 예약을 추가하면 결제 대기 상태로 예약 생성") { - val member = login(MemberFixture.create(role = Role.ADMIN)) - val adminRequest: AdminReservationCreateRequest = createRequest().let { - AdminReservationCreateRequest( - date = it.date, - themeId = it.themeId, - timeId = it.timeId, - memberId = member.id!!, - ) - } + val admin = testDataHelper.createMember(role = Role.ADMIN) + login(admin) + val theme = testDataHelper.createTheme() + val time = testDataHelper.createTime() + + val adminRequest = AdminReservationCreateRequest( + date = LocalDate.now().plusDays(1), + themeId = theme.id!!, + timeId = time.id!!, + memberId = admin.id!!, + ) Given { port(port) @@ -420,13 +407,13 @@ class ReservationControllerTest( } context("GET /reservations/waiting") { - lateinit var reservations: MutableMap> + lateinit var reservations: Map> beforeTest { - reservations = createDummyReservations() + reservations = testDataHelper.createDummyReservations(reservationCount = 5) } test("관리자가 아니면 조회할 수 없다.") { - login(MemberFixture.create(role = Role.MEMBER)) + login(testDataHelper.createMember(role = Role.MEMBER)) val expectedError = AuthErrorCode.ACCESS_DENIED Given { @@ -441,7 +428,7 @@ class ReservationControllerTest( } test("대기 중인 예약 목록을 조회한다.") { - login(MemberFixture.create(role = Role.ADMIN)) + login(testDataHelper.createMember(role = Role.ADMIN)) val expected = reservations.values.flatten() .count { it.status == ReservationStatus.WAITING } @@ -459,14 +446,16 @@ class ReservationControllerTest( context("POST /reservations/waiting") { test("회원이 대기 예약을 추가한다.") { - val member = login(MemberFixture.create(role = Role.MEMBER)) - val waitingCreateRequest: WaitingCreateRequest = createRequest().let { - WaitingCreateRequest( - date = it.date, - themeId = it.themeId, - timeId = it.timeId - ) - } + val member = testDataHelper.createMember(role = Role.MEMBER) + login(member) + val theme = testDataHelper.createTheme() + val time = testDataHelper.createTime() + + val waitingCreateRequest = WaitingCreateRequest( + date = LocalDate.now().plusDays(1), + themeId = theme.id!!, + timeId = time.id!! + ) Given { port(port) @@ -476,33 +465,30 @@ class ReservationControllerTest( post("/reservations/waiting") }.Then { statusCode(201) - body("data.member.id", equalTo(member.id!!.toInt())) + body("data.member.id", equalTo(member.id!!)) body("data.status", equalTo(ReservationStatus.WAITING.name)) } } test("이미 예약된 시간, 테마로 대기 예약 요청 시 예외 응답") { - val member = login(MemberFixture.create(role = Role.MEMBER)) - val reservationRequest = createRequest() + val member = testDataHelper.createMember(role = Role.MEMBER) + login(member) + val theme = testDataHelper.createTheme() + val time = testDataHelper.createTime() + val date = LocalDate.now().plusDays(1) - transactionTemplate.executeWithoutResult { - val reservation = ReservationFixture.create( - date = reservationRequest.date, - theme = entityManager.find(ThemeEntity::class.java, reservationRequest.themeId), - time = entityManager.find(TimeEntity::class.java, reservationRequest.timeId), - member = member, - status = ReservationStatus.WAITING - ) - entityManager.persist(reservation) - entityManager.flush() - entityManager.clear() - } + testDataHelper.createReservation( + date = date, + theme = theme, + time = time, + member = member, + status = ReservationStatus.CONFIRMED + ) - // 이미 예약된 시간, 테마로 대기 예약 요청 val waitingCreateRequest = WaitingCreateRequest( - date = reservationRequest.date, - themeId = reservationRequest.themeId, - timeId = reservationRequest.timeId + date = date, + themeId = theme.id!!, + timeId = time.id!! ) val expectedError = ReservationErrorCode.ALREADY_RESERVE @@ -520,14 +506,10 @@ class ReservationControllerTest( } context("DELETE /reservations/waiting/{id}") { - lateinit var reservations: MutableMap> - beforeTest { - reservations = createDummyReservations() - } - test("대기 중인 예약을 취소한다.") { - val member = login(MemberFixture.create(role = Role.MEMBER)) - val waiting: ReservationEntity = createSingleReservation( + val member = testDataHelper.createMember(role = Role.MEMBER) + login(member) + val waiting = testDataHelper.createReservation( member = member, status = ReservationStatus.WAITING ) @@ -540,17 +522,16 @@ class ReservationControllerTest( statusCode(HttpStatus.NO_CONTENT.value()) } - transactionTemplate.executeWithoutResult { _ -> - entityManager.find( - ReservationEntity::class.java, - waiting.id - ) shouldBe null + val deleted = transactionTemplate.execute { + entityManager.find(ReservationEntity::class.java, waiting.id) } + deleted shouldBe null } test("이미 확정된 예약을 삭제하면 예외 응답") { - val member = login(MemberFixture.create(role = Role.MEMBER)) - val reservation: ReservationEntity = createSingleReservation( + val member = testDataHelper.createMember(role = Role.MEMBER) + login(member) + val reservation = testDataHelper.createReservation( member = member, status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED ) @@ -559,7 +540,7 @@ class ReservationControllerTest( Given { port(port) }.When { - delete("/reservations/waiting/{id}", reservation.id) + delete("/reservations/waiting/${reservation.id}") }.Then { statusCode(expectedError.httpStatus.value()) body("code", equalTo(expectedError.errorCode)) @@ -569,7 +550,7 @@ class ReservationControllerTest( context("POST /reservations/waiting/{id}/confirm") { test("관리자만 승인할 수 있다.") { - login(MemberFixture.create(role = Role.MEMBER)) + login(testDataHelper.createMember(role = Role.MEMBER)) val expectedError = AuthErrorCode.ACCESS_DENIED Given { port(port) @@ -582,9 +563,8 @@ class ReservationControllerTest( } test("대기 예약을 승인하면 결제 대기 상태로 변경") { - val member = login(MemberFixture.create(role = Role.ADMIN)) - val reservation = createSingleReservation( - member = member, + login(testDataHelper.createMember(role = Role.ADMIN)) + val reservation = testDataHelper.createReservation( status = ReservationStatus.WAITING ) @@ -596,39 +576,28 @@ class ReservationControllerTest( statusCode(200) } - transactionTemplate.executeWithoutResult { _ -> - entityManager.find( - ReservationEntity::class.java, - reservation.id - )?.also { - it.status shouldBe ReservationStatus.CONFIRMED_PAYMENT_REQUIRED - } ?: throw AssertionError("Reservation not found") + val updatedReservation = transactionTemplate.execute { + entityManager.find(ReservationEntity::class.java, reservation.id) } + updatedReservation?.status shouldBe ReservationStatus.CONFIRMED_PAYMENT_REQUIRED } test("다른 확정된 예약을 승인하면 예외 응답") { - val admin = login(MemberFixture.create(role = Role.ADMIN)) - val alreadyReserved = createSingleReservation( + val admin = testDataHelper.createMember(role = Role.ADMIN) + login(admin) + val alreadyReserved = testDataHelper.createReservation( member = admin, status = ReservationStatus.CONFIRMED ) - val member = MemberFixture.create(account = "account", role = Role.MEMBER).also { it -> - transactionTemplate.executeWithoutResult { _ -> - entityManager.persist(it) - } - } - val waiting = ReservationFixture.create( + val member = testDataHelper.createMember(role = Role.MEMBER) + val waiting = testDataHelper.createReservation( date = alreadyReserved.date, time = alreadyReserved.time, theme = alreadyReserved.theme, member = member, status = ReservationStatus.WAITING - ).also { - transactionTemplate.executeWithoutResult { _ -> - entityManager.persist(it) - } - } + ) val expectedError = ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS Given { @@ -636,7 +605,6 @@ class ReservationControllerTest( }.When { post("/reservations/waiting/${waiting.id!!}/confirm") }.Then { - log().all() statusCode(expectedError.httpStatus.value()) body("code", equalTo(expectedError.errorCode)) } @@ -645,7 +613,7 @@ class ReservationControllerTest( context("POST /reservations/waiting/{id}/reject") { test("관리자만 거절할 수 있다.") { - login(MemberFixture.create(role = Role.MEMBER)) + login(testDataHelper.createMember(role = Role.MEMBER)) val expectedError = AuthErrorCode.ACCESS_DENIED Given { @@ -659,9 +627,8 @@ class ReservationControllerTest( } test("거절된 예약은 삭제된다.") { - val member = login(MemberFixture.create(role = Role.ADMIN)) - val reservation = createSingleReservation( - member = member, + login(testDataHelper.createMember(role = Role.ADMIN)) + val reservation = testDataHelper.createReservation( status = ReservationStatus.WAITING ) @@ -673,125 +640,91 @@ class ReservationControllerTest( statusCode(204) } - transactionTemplate.executeWithoutResult { _ -> - entityManager.find( - ReservationEntity::class.java, - reservation.id - ) shouldBe null + val rejected = transactionTemplate.execute { + entityManager.find(ReservationEntity::class.java, reservation.id) } + rejected shouldBe null } } } - - fun createSingleReservation( - date: LocalDate = LocalDate.now().plusDays(1), - time: LocalTime = LocalTime.now(), - themeName: String = "Default Theme", - member: MemberEntity = MemberFixture.create(role = Role.MEMBER), - status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED - ): ReservationEntity { - return ReservationFixture.create( - date = date, - theme = ThemeFixture.create(name = themeName), - time = TimeFixture.create(startAt = time), - member = member, - status = status - ).also { it -> - transactionTemplate.execute { _ -> - if (member.id == null) { - entityManager.persist(member) - } - entityManager.persist(it.time) - entityManager.persist(it.theme) - entityManager.persist(it) - entityManager.flush() - entityManager.clear() - } - } - } - - fun createDummyReservations(): MutableMap> { - val reservations: MutableMap> = mutableMapOf() - val members: List = listOf( - MemberFixture.create(role = Role.MEMBER), - MemberFixture.create(role = Role.MEMBER) - ) - - transactionTemplate.executeWithoutResult { - members.forEach { member -> - entityManager.persist(member) - } - entityManager.flush() - entityManager.clear() - } - - transactionTemplate.executeWithoutResult { - repeat(10) { index -> - val theme = ThemeFixture.create(name = "theme$index") - val time = TimeFixture.create(startAt = LocalTime.now().plusMinutes(index.toLong())) - entityManager.persist(theme) - entityManager.persist(time) - - val reservation = ReservationFixture.create( - date = LocalDate.now().plusDays(index.toLong()), - theme = theme, - time = time, - member = members[index % members.size], - status = ReservationStatus.CONFIRMED - ) - entityManager.persist(reservation) - reservations.getOrPut(reservation.member) { mutableListOf() }.add(reservation) - } - entityManager.flush() - entityManager.clear() - } - - return reservations - } - - fun createRequest( - theme: ThemeEntity = ThemeFixture.create(), - time: TimeEntity = TimeFixture.create(), - ): ReservationCreateWithPaymentRequest { - lateinit var reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest - - transactionTemplate.executeWithoutResult { - entityManager.persist(theme) - entityManager.persist(time) - - reservationCreateWithPaymentRequest = ReservationFixture.createRequest( - themeId = theme.id!!, - timeId = time.id!!, - ) - - entityManager.flush() - entityManager.clear() - } - - return reservationCreateWithPaymentRequest - } - - fun login(member: MemberEntity): MemberEntity { - if (member.id == null) { - transactionTemplate.executeWithoutResult { - entityManager.persist(member) - entityManager.flush() - entityManager.clear() - } - } - - every { - jwtHandler.getMemberIdFromToken(any()) - } returns member.id!! - - every { - memberService.findById(member.id!!) - } returns member - - every { - memberIdResolver.resolveArgument(any(), any(), any(), any()) - } returns member.id!! - - return member - } } + +class TestDataHelper( + private val entityManager: EntityManager, + private val transactionTemplate: TransactionTemplate, +) { + private var memberSequence = 0L + private var themeSequence = 0L + private var timeSequence = 0L + + fun createMember( + role: Role = Role.MEMBER, + account: String = "member${++memberSequence}@test.com", + ): MemberEntity { + val member = MemberFixture.create(role = role, account = account) + return persist(member) + } + + fun createTheme(name: String = "theme-${++themeSequence}"): ThemeEntity { + val theme = ThemeFixture.create(name = name) + return persist(theme) + } + + fun createTime(startAt: LocalTime = LocalTime.of(10, 0).plusMinutes(++timeSequence * 10)): TimeEntity { + val time = TimeFixture.create(startAt = startAt) + return persist(time) + } + + fun createReservation( + date: LocalDate = LocalDate.now().plusDays(1), + theme: ThemeEntity = createTheme(), + time: TimeEntity = createTime(), + member: MemberEntity = createMember(), + status: ReservationStatus = ReservationStatus.CONFIRMED, + ): ReservationEntity { + val reservation = ReservationFixture.create( + date = date, + theme = theme, + time = time, + member = member, + status = status + ) + return persist(reservation) + } + + fun createPayment(reservation: ReservationEntity): PaymentEntity { + val payment = PaymentFixture.create(reservation = reservation) + return persist(payment) + } + + fun createReservationRequest( + theme: ThemeEntity = createTheme(), + time: TimeEntity = createTime(), + ): ReservationCreateWithPaymentRequest { + return ReservationFixture.createRequest( + themeId = theme.id!!, + timeId = time.id!!, + ) + } + + fun createDummyReservations( + memberCount: Int = 2, + reservationCount: Int = 10, + ): Map> { + val members = (1..memberCount).map { createMember(role = Role.MEMBER) } + val reservations = (1..reservationCount).map { index -> + createReservation( + member = members[index % memberCount], + status = ReservationStatus.CONFIRMED + ) + } + return reservations.groupBy { it.member } + } + + private fun persist(entity: T): T { + transactionTemplate.executeWithoutResult { + entityManager.persist(entity) + } + return entity + } +} \ No newline at end of file diff --git a/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt b/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt index 80a534e7..4cecc9d1 100644 --- a/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt +++ b/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt @@ -13,12 +13,13 @@ import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeRepository import roomescape.theme.web.ThemeCreateRequest import roomescape.theme.web.ThemeRetrieveResponse +import roomescape.util.TsidFactory import roomescape.util.ThemeFixture class ThemeServiceTest : FunSpec({ val themeRepository: ThemeRepository = mockk() - val themeService = ThemeService(themeRepository) + val themeService = ThemeService(TsidFactory, themeRepository) context("findThemeById") { val themeId = 1L diff --git a/src/test/kotlin/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt b/src/test/kotlin/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt index bc7d35de..2784f4ea 100644 --- a/src/test/kotlin/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt +++ b/src/test/kotlin/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt @@ -8,7 +8,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import roomescape.theme.util.TestThemeCreateUtil import java.time.LocalDate -@DataJpaTest +@DataJpaTest(showSql = false) class ThemeRepositoryTest( val themeRepository: ThemeRepository, val entityManager: EntityManager diff --git a/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt b/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt index 7a6a130d..a6cc1dff 100644 --- a/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt +++ b/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt @@ -10,6 +10,7 @@ import io.mockk.just import io.mockk.runs import org.hamcrest.Matchers.equalTo import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import roomescape.auth.exception.AuthErrorCode diff --git a/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt b/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt index 125976bf..73266aaf 100644 --- a/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt +++ b/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt @@ -14,6 +14,7 @@ import roomescape.time.exception.TimeErrorCode import roomescape.time.exception.TimeException import roomescape.time.infrastructure.persistence.TimeRepository import roomescape.time.web.TimeCreateRequest +import roomescape.util.TsidFactory import roomescape.util.TimeFixture import java.time.LocalTime @@ -22,6 +23,7 @@ class TimeServiceTest : FunSpec({ val reservationRepository: ReservationRepository = mockk() val timeService = TimeService( + tsidFactory = TsidFactory, timeRepository = timeRepository, reservationRepository = reservationRepository ) diff --git a/src/test/kotlin/roomescape/time/infrastructure/persistence/TimeRepositoryTest.kt b/src/test/kotlin/roomescape/time/infrastructure/persistence/TimeRepositoryTest.kt index 5c99b07d..bc79357b 100644 --- a/src/test/kotlin/roomescape/time/infrastructure/persistence/TimeRepositoryTest.kt +++ b/src/test/kotlin/roomescape/time/infrastructure/persistence/TimeRepositoryTest.kt @@ -7,7 +7,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import roomescape.util.TimeFixture import java.time.LocalTime -@DataJpaTest +@DataJpaTest(showSql = false) class TimeRepositoryTest( val entityManager: EntityManager, val timeRepository: TimeRepository, diff --git a/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt b/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt index b9ef49a9..c5a57e44 100644 --- a/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt +++ b/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt @@ -9,6 +9,7 @@ import io.mockk.every import org.hamcrest.Matchers.equalTo import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.context.annotation.Import +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext import org.springframework.data.repository.findByIdOrNull import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc @@ -27,7 +28,6 @@ import java.time.LocalDate import java.time.LocalTime @WebMvcTest(TimeController::class) -@Import(JacksonConfig::class) class TimeControllerTest( val mockMvc: MockMvc, ) : RoomescapeApiTest() { diff --git a/src/test/kotlin/roomescape/util/Fixtures.kt b/src/test/kotlin/roomescape/util/Fixtures.kt index 43fa2824..526e3d16 100644 --- a/src/test/kotlin/roomescape/util/Fixtures.kt +++ b/src/test/kotlin/roomescape/util/Fixtures.kt @@ -1,7 +1,9 @@ package roomescape.util +import com.github.f4b6a3.tsid.TsidFactory import roomescape.auth.infrastructure.jwt.JwtHandler import roomescape.auth.web.LoginRequest +import roomescape.common.config.next import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.Role import roomescape.payment.infrastructure.client.PaymentApproveRequest @@ -20,11 +22,14 @@ import java.time.LocalDate import java.time.LocalTime import java.time.OffsetDateTime + +val TsidFactory: TsidFactory = TsidFactory(0) + object MemberFixture { const val NOT_LOGGED_IN_USERID: Long = 0 fun create( - id: Long? = null, + id: Long? = TsidFactory.next(), name: String = "sangdol", account: String = "default", password: String = "password", @@ -56,14 +61,14 @@ object MemberFixture { object TimeFixture { fun create( - id: Long? = null, + id: Long? = TsidFactory.next(), startAt: LocalTime = LocalTime.now().plusHours(1), ): TimeEntity = TimeEntity(id, startAt) } object ThemeFixture { fun create( - id: Long? = null, + id: Long? = TsidFactory.next(), name: String = "Default Theme", description: String = "Default Description", thumbnail: String = "https://example.com/default-thumbnail.jpg" @@ -72,7 +77,7 @@ object ThemeFixture { object ReservationFixture { fun create( - id: Long? = null, + id: Long? = TsidFactory.next(), date: LocalDate = LocalDate.now().plusWeeks(1), theme: ThemeEntity = ThemeFixture.create(), time: TimeEntity = TimeFixture.create(), @@ -125,14 +130,14 @@ object PaymentFixture { const val AMOUNT: Long = 10000L fun create( - id: Long? = null, + id: Long? = TsidFactory.next(), orderId: String = ORDER_ID, paymentKey: String = PAYMENT_KEY, totalAmount: Long = AMOUNT, reservation: ReservationEntity = ReservationFixture.create(id = 1L), approvedAt: OffsetDateTime = OffsetDateTime.now() ): PaymentEntity = PaymentEntity( - id = id, + _id = id, orderId = orderId, paymentKey = paymentKey, totalAmount = totalAmount, @@ -141,14 +146,14 @@ object PaymentFixture { ) fun createCanceled( - id: Long? = null, + id: Long? = TsidFactory.next(), paymentKey: String = PAYMENT_KEY, cancelReason: String = "Test Cancel", cancelAmount: Long = AMOUNT, approvedAt: OffsetDateTime = OffsetDateTime.now(), canceledAt: OffsetDateTime = approvedAt.plusHours(1) ): CanceledPaymentEntity = CanceledPaymentEntity( - id = id, + _id = id, paymentKey = paymentKey, cancelReason = cancelReason, cancelAmount = cancelAmount, diff --git a/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt b/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt index f5e40349..9a038941 100644 --- a/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt +++ b/src/test/kotlin/roomescape/util/RoomescapeApiTest.kt @@ -1,10 +1,16 @@ package roomescape.util import com.fasterxml.jackson.databind.ObjectMapper +import com.github.f4b6a3.tsid.TsidFactory import com.ninjasquad.springmockk.MockkBean import com.ninjasquad.springmockk.SpykBean import io.kotest.core.spec.style.BehaviorSpec import io.mockk.every +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Import +import org.springframework.context.annotation.Primary +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpHeaders import org.springframework.http.MediaType @@ -21,6 +27,8 @@ import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.MemberRepository import roomescape.util.MemberFixture.NOT_LOGGED_IN_USERID +@Import(TestConfig::class, JacksonConfig::class) +@MockkBean(JpaMetamodelMappingContext::class) abstract class RoomescapeApiTest : BehaviorSpec() { @SpykBean @@ -128,3 +136,10 @@ abstract class RoomescapeApiTest : BehaviorSpec() { """.trimIndent() ) } + +@TestConfiguration +class TestConfig { + @Bean + @Primary + fun tsidFactory(): TsidFactory = TsidFactory +} \ No newline at end of file