[#30] 코드 구조 개선 #31

Merged
pricelees merged 31 commits from refactor/#30 into main 2025-08-06 10:16:08 +00:00
5 changed files with 338 additions and 105 deletions
Showing only changes of commit 393dceb355 - Show all commits

View File

@ -4,114 +4,162 @@ import io.kotest.assertions.assertSoftly
import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import io.mockk.every import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs import io.mockk.slot
import roomescape.payment.exception.PaymentErrorCode import roomescape.payment.exception.PaymentErrorCode
import roomescape.payment.exception.PaymentException import roomescape.payment.exception.PaymentException
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository import roomescape.payment.implement.PaymentFinder
import roomescape.payment.infrastructure.persistence.PaymentRepository import roomescape.payment.implement.PaymentWriter
import roomescape.payment.infrastructure.client.PaymentApproveResponse
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
import roomescape.payment.web.PaymentCancelRequest import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentCancelResponse
import roomescape.util.PaymentFixture import roomescape.util.PaymentFixture
import roomescape.util.TsidFactory import roomescape.util.ReservationFixture
import java.time.LocalDate
import java.time.LocalTime
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset
class PaymentServiceTest : FunSpec({ class PaymentServiceTest : FunSpec({
val paymentRepository: PaymentRepository = mockk() val paymentFinder: PaymentFinder = mockk()
val canceledPaymentRepository: CanceledPaymentRepository = mockk() val paymentWriter: PaymentWriter = mockk()
val paymentService = PaymentService(TsidFactory, paymentRepository, canceledPaymentRepository) val paymentService = PaymentService(paymentFinder, paymentWriter)
context("createCanceledPaymentByReservationId") { context("createPayment") {
val reservationId = 1L val approvedPaymentInfo = PaymentApproveResponse(
test("reservationId로 paymentKey를 찾을 수 없으면 예외를 던진다.") { paymentKey = "paymentKey",
every { paymentRepository.findPaymentKeyByReservationId(reservationId) } returns null orderId = "orderId",
totalAmount = 1000L,
approvedAt = OffsetDateTime.now(),
)
val reservation = ReservationFixture.create(id = 1L)
val exception = shouldThrow<PaymentException> { test("정상 응답") {
paymentService.createCanceledPaymentByReservationId(reservationId) every {
paymentWriter.create(
paymentKey = approvedPaymentInfo.paymentKey,
orderId = approvedPaymentInfo.orderId,
totalAmount = approvedPaymentInfo.totalAmount,
approvedAt = approvedPaymentInfo.approvedAt,
reservation = reservation
)
} returns PaymentFixture.create(
id = 1L,
orderId = approvedPaymentInfo.orderId,
paymentKey = approvedPaymentInfo.paymentKey,
totalAmount = approvedPaymentInfo.totalAmount,
approvedAt = approvedPaymentInfo.approvedAt,
reservation = reservation
)
val response = paymentService.createPayment(approvedPaymentInfo, reservation)
assertSoftly(response) {
it.id shouldBe 1L
it.paymentKey shouldBe approvedPaymentInfo.paymentKey
it.reservationId shouldBe reservation.id
} }
exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
} }
}
context("reservationId로 paymentKey를 찾고난 후") { context("createCanceledPayment(canceledPaymentInfo)") {
val paymentKey = "test-payment-key" val canceledPaymentInfo = PaymentCancelResponse(
cancelStatus = "normal",
cancelReason = "고객 요청",
cancelAmount = 1000L,
canceledAt = OffsetDateTime.now(),
)
val approvedAt = OffsetDateTime.now()
val paymentKey = "paymentKey"
test("CanceledPaymentEntity를 응답") {
every {
paymentWriter.createCanceled(
cancelReason = canceledPaymentInfo.cancelReason,
cancelAmount = canceledPaymentInfo.cancelAmount,
canceledAt = canceledPaymentInfo.canceledAt,
approvedAt = approvedAt,
paymentKey = paymentKey
)
} returns PaymentFixture.createCanceled(
id = 1L,
paymentKey = paymentKey,
cancelAmount = canceledPaymentInfo.cancelAmount
)
val response = paymentService.createCanceledPayment(canceledPaymentInfo, approvedAt, paymentKey)
response.shouldBeInstanceOf<CanceledPaymentEntity>()
response.paymentKey shouldBe paymentKey
}
}
context("createCanceledPayment(reservationId)") {
val reservationId = 1L
test("취소 사유를 '예약 취소'로 하여 PaymentCancelRequest를 응답") {
val payment = PaymentFixture.create(id = 1L, paymentKey = "paymentKey", totalAmount = 1000L)
every {
paymentFinder.findByReservationId(reservationId)
} returns payment
val cancelReasonSlot = slot<String>()
every { every {
paymentRepository.findPaymentKeyByReservationId(reservationId) paymentWriter.createCanceled(payment, capture(cancelReasonSlot), any())
} returns paymentKey } returns PaymentFixture.createCanceled(
id = 1L,
paymentKey = payment.paymentKey,
cancelAmount = payment.totalAmount
)
test("해당 paymentKey로 paymentEntity를 찾을 수 없으면 예외를 던진다.") { val response = paymentService.createCanceledPayment(reservationId)
every {
paymentRepository.findByPaymentKey(paymentKey)
} returns null
val exception = shouldThrow<PaymentException> { response.shouldBeInstanceOf<PaymentCancelRequest>()
paymentService.createCanceledPaymentByReservationId(reservationId) cancelReasonSlot.captured shouldBe "예약 취소"
} }
exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
}
test("해당 paymentKey로 paymentEntity를 찾고, cancelPaymentEntity를 저장한다.") { test("결제 정보가 없으면 예외 응답") {
val paymentEntity = PaymentFixture.create(paymentKey = paymentKey) every {
paymentFinder.findByReservationId(reservationId)
} throws PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
every { shouldThrow<PaymentException> {
paymentRepository.findByPaymentKey(paymentKey) paymentService.createCanceledPayment(reservationId)
} returns paymentEntity.also { }.also {
every { it.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
paymentRepository.delete(it)
} just runs
}
every {
canceledPaymentRepository.save(any())
} returns PaymentFixture.createCanceled(
id = 1L,
paymentKey = paymentKey,
cancelReason = "Test",
cancelAmount = paymentEntity.totalAmount,
)
val result: PaymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
assertSoftly(result) {
this.paymentKey shouldBe paymentKey
this.amount shouldBe paymentEntity.totalAmount
this.cancelReason shouldBe "Test"
}
} }
} }
} }
context("updateCanceledTime") { context("updateCanceledTime") {
val paymentKey = "test-payment-key" val paymentKey = "paymentKey"
val canceledAt = OffsetDateTime.now() val canceledAt = OffsetDateTime.of(LocalDate.of(2025, 8, 5), LocalTime.of(10, 0), ZoneOffset.UTC)
test("paymentKey로 canceledPaymentEntity를 찾을 수 없으면 예외를 던진다.") { test("정상 응답") {
val canceled = PaymentFixture.createCanceled(id = 1L)
every { every {
canceledPaymentRepository.findByPaymentKey(paymentKey) paymentFinder.findCanceledByKey(paymentKey)
} returns null } returns canceled
val exception = shouldThrow<PaymentException> {
paymentService.updateCanceledTime(paymentKey, canceledAt)
}
exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
}
test("paymentKey로 canceledPaymentEntity를 찾고, canceledAt을 업데이트한다.") {
val canceledPaymentEntity = PaymentFixture.createCanceled(
paymentKey = paymentKey,
canceledAt = canceledAt.minusMinutes(1)
)
every {
canceledPaymentRepository.findByPaymentKey(paymentKey)
} returns canceledPaymentEntity
paymentService.updateCanceledTime(paymentKey, canceledAt) paymentService.updateCanceledTime(paymentKey, canceledAt)
assertSoftly(canceledPaymentEntity) { canceled.canceledAt shouldBe canceledAt
this.canceledAt shouldBe canceledAt }
test("결제 취소 정보가 없으면 예외 응답") {
every {
paymentFinder.findCanceledByKey(paymentKey)
} throws PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
shouldThrow<PaymentException> {
paymentService.updateCanceledTime(paymentKey, canceledAt)
}.also {
it.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
} }
} }
} }

View File

@ -0,0 +1,93 @@
package roomescape.payment.implement
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import roomescape.payment.exception.PaymentErrorCode
import roomescape.payment.exception.PaymentException
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
import roomescape.payment.infrastructure.persistence.PaymentRepository
class PaymentFinderTest : FunSpec({
val paymentRepository: PaymentRepository = mockk()
val canceledPaymentRepository: CanceledPaymentRepository = mockk()
val paymentFinder = PaymentFinder(paymentRepository, canceledPaymentRepository)
context("existsPaymentByReservationId") {
val reservationId = 1L
test("결제 정보가 있으면 true를 반환한다.") {
every {
paymentRepository.existsByReservationId(reservationId)
} returns true
paymentFinder.existsPaymentByReservationId(reservationId) shouldBe true
}
test("결제 정보가 없으면 false를 반환한다.") {
every {
paymentRepository.existsByReservationId(reservationId)
} returns false
paymentFinder.existsPaymentByReservationId(reservationId) shouldBe false
}
}
context("findByReservationId") {
val reservationId = 1L
test("결제 정보를 조회한다.") {
every {
paymentRepository.findByReservationId(reservationId)
} returns mockk()
paymentFinder.findByReservationId(reservationId)
verify(exactly = 1) {
paymentRepository.findByReservationId(reservationId)
}
}
test("결제 정보가 없으면 실패한다.") {
every {
paymentRepository.findByReservationId(reservationId)
} returns null
shouldThrow<PaymentException> {
paymentFinder.findByReservationId(reservationId)
}.also {
it.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
}
}
}
context("findCanceledByKey") {
val paymentKey = "paymentKey"
test("결제 취소 정보를 조회한다.") {
every {
canceledPaymentRepository.findByPaymentKey(paymentKey)
} returns mockk()
paymentFinder.findCanceledByKey(paymentKey)
verify(exactly = 1) {
canceledPaymentRepository.findByPaymentKey(paymentKey)
}
}
test("결제 취소 정보가 없으면 실패한다.") {
every {
canceledPaymentRepository.findByPaymentKey(paymentKey)
} returns null
shouldThrow<PaymentException> {
paymentFinder.findCanceledByKey(paymentKey)
}.also {
it.errorCode shouldBe PaymentErrorCode.CANCELED_PAYMENT_NOT_FOUND
}
}
}
})

View File

@ -0,0 +1,114 @@
package roomescape.payment.implement
import com.ninjasquad.springmockk.MockkClear
import com.ninjasquad.springmockk.clear
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.date.after
import io.kotest.matchers.shouldBe
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
import roomescape.payment.infrastructure.persistence.PaymentEntity
import roomescape.payment.infrastructure.persistence.PaymentRepository
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.util.PaymentFixture
import roomescape.util.ReservationFixture
import roomescape.util.TsidFactory
import java.time.OffsetDateTime
class PaymentWriterTest : FunSpec({
val paymentRepository: PaymentRepository = mockk()
val canceledPaymentRepository: CanceledPaymentRepository = mockk()
val paymentWriter = PaymentWriter(paymentRepository, canceledPaymentRepository, TsidFactory)
val paymentKey = "paymentKey"
val orderId = "orderId"
val totalAmount = 1000L
val approvedAt = OffsetDateTime.now()
context("create") {
val reservation: ReservationEntity = ReservationFixture.create(id = 1L)
test("결제 정보를 저장한다.") {
val slot = slot<PaymentEntity>()
every {
paymentRepository.save(capture(slot))
} returns mockk()
paymentWriter.create(paymentKey, orderId, totalAmount, approvedAt, reservation)
verify(exactly = 1) {
paymentRepository.save(any())
}
slot.captured.also {
it.paymentKey shouldBe paymentKey
it.orderId shouldBe orderId
it.totalAmount shouldBe totalAmount
it.approvedAt shouldBe approvedAt
}
paymentRepository.clear(MockkClear.AFTER)
}
}
context("createCanceled") {
val cancelReason = "고객 요청"
val canceledAt = OffsetDateTime.now()
afterTest {
clearMocks(canceledPaymentRepository)
}
test("PaymentEntity를 받아 저장한다.") {
val payment: PaymentEntity = PaymentFixture.create(id = 1L)
val slot = slot<CanceledPaymentEntity>()
every {
canceledPaymentRepository.save(capture(slot))
} returns mockk()
paymentWriter.createCanceled(payment, cancelReason, canceledAt)
verify(exactly = 1) {
canceledPaymentRepository.save(any())
}
slot.captured.also {
it.paymentKey shouldBe payment.paymentKey
it.cancelAmount shouldBe payment.totalAmount
it.approvedAt shouldBe payment.approvedAt
}
}
test("취소 정보를 받아 저장한다.") {
val slot = slot<CanceledPaymentEntity>()
every {
canceledPaymentRepository.save(capture(slot))
} returns mockk()
paymentWriter.createCanceled(
cancelReason = cancelReason,
cancelAmount = totalAmount,
canceledAt = canceledAt,
approvedAt = approvedAt,
paymentKey = paymentKey
)
verify(exactly = 1) {
canceledPaymentRepository.save(any())
}
slot.captured.also {
it.paymentKey shouldBe paymentKey
it.cancelAmount shouldBe totalAmount
it.approvedAt shouldBe approvedAt
}
}
}
})

View File

@ -39,28 +39,6 @@ class PaymentRepositoryTest(
} }
} }
context("findPaymentKeyByReservationId") {
lateinit var paymentKey: String
beforeTest {
reservation = setupReservation()
paymentKey = PaymentFixture.create(reservation = reservation)
.also { paymentRepository.save(it) }
.paymentKey
}
test("정상 반환") {
paymentRepository.findPaymentKeyByReservationId(reservation.id!!)
?.let { it shouldBe paymentKey }
?: throw AssertionError("Unexpected null value")
}
test("null 반환") {
paymentRepository.findPaymentKeyByReservationId(reservation.id!! + 1)
.also { it shouldBe null }
}
}
context("findByPaymentKey") { context("findByPaymentKey") {
lateinit var payment: PaymentEntity lateinit var payment: PaymentEntity

View File

@ -13,8 +13,8 @@ import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.toCreateResponse import roomescape.payment.web.toCreateResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.reservation.infrastructure.persistence.ReservationStatus
import roomescape.reservation.web.ReservationCreateResponse
import roomescape.reservation.web.ReservationCreateWithPaymentRequest import roomescape.reservation.web.ReservationCreateWithPaymentRequest
import roomescape.reservation.web.ReservationRetrieveResponse
import roomescape.util.* import roomescape.util.*
class ReservationWithPaymentServiceTest : FunSpec({ class ReservationWithPaymentServiceTest : FunSpec({
@ -55,9 +55,9 @@ class ReservationWithPaymentServiceTest : FunSpec({
paymentService.createPayment(paymentApproveResponse, reservationEntity) paymentService.createPayment(paymentApproveResponse, reservationEntity)
} returns paymentEntity.toCreateResponse() } returns paymentEntity.toCreateResponse()
val result: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment( val result: ReservationCreateResponse = reservationWithPaymentService.createReservationAndPayment(
request = reservationCreateWithPaymentRequest, request = reservationCreateWithPaymentRequest,
paymentInfo = paymentApproveResponse, approvedPaymentInfo = paymentApproveResponse,
memberId = memberId memberId = memberId
) )
@ -81,7 +81,7 @@ class ReservationWithPaymentServiceTest : FunSpec({
) )
every { every {
paymentService.createCanceledPaymentByReservationId(reservationEntity.id!!) paymentService.createCanceledPayment(reservationEntity.id!!)
} returns paymentCancelRequest } returns paymentCancelRequest
every { every {
@ -100,7 +100,7 @@ class ReservationWithPaymentServiceTest : FunSpec({
context("isNotPaidReservation") { context("isNotPaidReservation") {
test("결제된 예약이면 true를 반환한다.") { test("결제된 예약이면 true를 반환한다.") {
every { every {
paymentService.isReservationPaid(reservationEntity.id!!) paymentService.existsByReservationId(reservationEntity.id!!)
} returns false } returns false
val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!) val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!)
@ -110,7 +110,7 @@ class ReservationWithPaymentServiceTest : FunSpec({
test("결제되지 않은 예약이면 false를 반환한다.") { test("결제되지 않은 예약이면 false를 반환한다.") {
every { every {
paymentService.isReservationPaid(reservationEntity.id!!) paymentService.existsByReservationId(reservationEntity.id!!)
} returns true } returns true
val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!) val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!)