From 6d87200079b5b3a073a9b51f649782fbe78fb8f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=95=B8=EB=AA=A8?= Date: Sat, 24 Feb 2024 22:37:48 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=82=B0=ED=83=80=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/christmas/Application.kt | 11 +- .../src/main/kotlin/christmas/badge/Badge.kt | 22 ++++ .../controller/ChristmasController.kt | 18 +++ .../kotlin/christmas/event/EventPolicy.kt | 109 ++++++++++++++++++ .../main/kotlin/christmas/event/EventType.kt | 16 +++ .../src/main/kotlin/christmas/menu/Menu.kt | 34 ++++++ .../src/main/kotlin/christmas/model/Price.kt | 27 +++++ .../kotlin/christmas/model/UserBenefitInfo.kt | 25 ++++ .../kotlin/christmas/model/UserOrderInfo.kt | 38 ++++++ .../christmas/service/ChristmasService.kt | 22 ++++ .../christmas/validation/LottoValidation.kt | 17 +++ .../main/kotlin/christmas/view/InputView.kt | 61 ++++++++++ .../main/kotlin/christmas/view/OutputView.kt | 42 +++++++ 13 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 kotlin-christmas/src/main/kotlin/christmas/badge/Badge.kt create mode 100644 kotlin-christmas/src/main/kotlin/christmas/controller/ChristmasController.kt create mode 100644 kotlin-christmas/src/main/kotlin/christmas/event/EventPolicy.kt create mode 100644 kotlin-christmas/src/main/kotlin/christmas/event/EventType.kt create mode 100644 kotlin-christmas/src/main/kotlin/christmas/menu/Menu.kt create mode 100644 kotlin-christmas/src/main/kotlin/christmas/model/Price.kt create mode 100644 kotlin-christmas/src/main/kotlin/christmas/model/UserBenefitInfo.kt create mode 100644 kotlin-christmas/src/main/kotlin/christmas/model/UserOrderInfo.kt create mode 100644 kotlin-christmas/src/main/kotlin/christmas/service/ChristmasService.kt create mode 100644 kotlin-christmas/src/main/kotlin/christmas/validation/LottoValidation.kt create mode 100644 kotlin-christmas/src/main/kotlin/christmas/view/InputView.kt create mode 100644 kotlin-christmas/src/main/kotlin/christmas/view/OutputView.kt diff --git a/kotlin-christmas/src/main/kotlin/christmas/Application.kt b/kotlin-christmas/src/main/kotlin/christmas/Application.kt index 8d101ce..bd62cdf 100644 --- a/kotlin-christmas/src/main/kotlin/christmas/Application.kt +++ b/kotlin-christmas/src/main/kotlin/christmas/Application.kt @@ -1,5 +1,14 @@ package christmas +import christmas.controller.ChristmasController +import christmas.service.ChristmasService +import christmas.view.InputView +import christmas.view.OutputView + fun main() { - TODO("프로그램 구현") + ChristmasController.run( + InputView, + OutputView, + ChristmasService + ) } diff --git a/kotlin-christmas/src/main/kotlin/christmas/badge/Badge.kt b/kotlin-christmas/src/main/kotlin/christmas/badge/Badge.kt new file mode 100644 index 0000000..0bf8727 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/badge/Badge.kt @@ -0,0 +1,22 @@ +package christmas.badge + +import christmas.model.Price +import christmas.validation.ChristmasException + +enum class Badge(val minBenefitAmount: UInt) { + 별(5_000u), + 트리(10_000u), + 산타(20_000u), + ; + + companion object { + fun of(benefitAmount: Price) = + when (benefitAmount.value.toUInt()) { + in 0u until 별.minBenefitAmount -> null + in 별.minBenefitAmount until 트리.minBenefitAmount -> 별 + in 트리.minBenefitAmount until 산타.minBenefitAmount -> 트리 + in 산타.minBenefitAmount until UInt.MAX_VALUE -> 산타 + else -> throw ChristmasException("말도 안되는 혜택 금액입니다. 혜택 금액: $benefitAmount") + } + } +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/controller/ChristmasController.kt b/kotlin-christmas/src/main/kotlin/christmas/controller/ChristmasController.kt new file mode 100644 index 0000000..8d41e86 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/controller/ChristmasController.kt @@ -0,0 +1,18 @@ +package christmas.controller + +import christmas.service.ChristmasService +import christmas.view.InputView +import christmas.view.OutputView + +object ChristmasController { + fun run( + inputView: InputView, + outputView: OutputView, + christmasService: ChristmasService, + ) { + outputView.printWelcomeMessage() + val userOrderInfo = inputView.getUserOrderInfo() + val benefitInfo = christmasService.getBenefitInfo(userOrderInfo) + outputView.printUserEventBenefit(userOrderInfo, benefitInfo) + } +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/event/EventPolicy.kt b/kotlin-christmas/src/main/kotlin/christmas/event/EventPolicy.kt new file mode 100644 index 0000000..3494482 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/event/EventPolicy.kt @@ -0,0 +1,109 @@ +package christmas.event + +import christmas.menu.Menu +import christmas.menu.MenuType +import christmas.menu.MenuWithCount +import christmas.model.Price +import christmas.model.UserOrderInfo +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.Month + +private const val MIN_EVENT_PRICE = 10_000 + +enum class EventPolicy { + GiveawayEventPolicy { + override fun isOwnSupported(userOrderInfo: UserOrderInfo) = userOrderInfo.totalPrice >= 120_000 + + override fun getBenefit(userOrderInfo: UserOrderInfo): EventType = + EventType.Giveaway(MenuWithCount(Menu.샴페인, 1), this.getPolicyName()) + + override fun getPolicyName() = "증정 이벤트" + }, + + ChristmasDiscountPolicy { + override fun isOwnSupported(userOrderInfo: UserOrderInfo) = + userOrderInfo.estimatedVisitDate.isBefore(LocalDate.of(2023, 12, 26)) + + override fun getBenefit(userOrderInfo: UserOrderInfo) = + EventType.Discount( + Price(1000 + 100 * (userOrderInfo.estimatedVisitDate.dayOfMonth - 1)), + this.getPolicyName() + ) + + override fun getPolicyName() = "크리스마스 디데이 할인" + }, + + WeekdayDiscountPolicy { + private val discountPricePerMenu = 2023 + private val availableDiscountDayOfWeek = listOf( + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY + ) + + override fun isOwnSupported(userOrderInfo: UserOrderInfo) = + availableDiscountDayOfWeek.contains(userOrderInfo.estimatedVisitDate.dayOfWeek) + + override fun getBenefit(userOrderInfo: UserOrderInfo) = + EventType.Discount( + Price(userOrderInfo.getSumPriceOfMenu(MenuType.Dessert) * discountPricePerMenu), + this.getPolicyName() + ) + + override fun getPolicyName() = "평일 할인" + }, + + WeekendDiscountPolicy { + private val discountPricePerMenu = 2023 + private val availableDiscountDayOfWeek = listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY) + + override fun isOwnSupported(userOrderInfo: UserOrderInfo) = + availableDiscountDayOfWeek.contains(userOrderInfo.estimatedVisitDate.dayOfWeek) + + override fun getBenefit(userOrderInfo: UserOrderInfo) = + EventType.Discount( + Price(userOrderInfo.getSumPriceOfMenu(MenuType.Main) * discountPricePerMenu), + this.getPolicyName() + ) + + override fun getPolicyName() = "주말 할인" + }, + + SpecialDiscountPolicy { + private val discountPrice = 1000 + private val availableDiscountDay = listOf( + LocalDate.of(2023, 12, 3), + LocalDate.of(2023, 12, 10), + LocalDate.of(2023, 12, 17), + LocalDate.of(2023, 12, 24), + LocalDate.of(2023, 12, 25), + LocalDate.of(2023, 12, 31), + ) + + override fun isOwnSupported(userOrderInfo: UserOrderInfo) = + availableDiscountDay.contains(userOrderInfo.estimatedVisitDate) + + override fun getBenefit(userOrderInfo: UserOrderInfo) = + EventType.Discount(Price(discountPrice), this.getPolicyName()) + + override fun getPolicyName() = "특별 할인" + }, + ; + + fun isSupported(userOrderInfo: UserOrderInfo) = + userOrderInfo.totalPrice >= MIN_EVENT_PRICE && + isInEventDate(userOrderInfo) && + isOwnSupported(userOrderInfo) + + private fun isInEventDate(userOrderInfo: UserOrderInfo) = + userOrderInfo.estimatedVisitDate.year == 2023 && userOrderInfo.estimatedVisitDate.month == Month.DECEMBER + + protected abstract fun isOwnSupported(userOrderInfo: UserOrderInfo): Boolean + + abstract fun getBenefit(userOrderInfo: UserOrderInfo): EventType + + protected abstract fun getPolicyName(): String +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/event/EventType.kt b/kotlin-christmas/src/main/kotlin/christmas/event/EventType.kt new file mode 100644 index 0000000..2ddf7d6 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/event/EventType.kt @@ -0,0 +1,16 @@ +package christmas.event + +import christmas.menu.MenuWithCount +import christmas.model.Price + +sealed class EventType( + private val benefitAmount: Price, + private val eventPolicyName: String +) { + class Discount(val price: Price, eventPolicyName: String) : EventType(price, eventPolicyName) + + class Giveaway(val menuWithCount: MenuWithCount, eventPolicyName: String) : + EventType(menuWithCount.benefitAmount, eventPolicyName) + + override fun toString() = "${eventPolicyName}: ${benefitAmount.toMinusString()}" +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/menu/Menu.kt b/kotlin-christmas/src/main/kotlin/christmas/menu/Menu.kt new file mode 100644 index 0000000..3cdf5f8 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/menu/Menu.kt @@ -0,0 +1,34 @@ +package christmas.menu + +import christmas.model.Price + +enum class Menu(val price: Price, val menuType: MenuType) { + 양송이스프(Price(6_000), MenuType.Appetizer), + 타파스(Price(5_500), MenuType.Appetizer), + 시저샐러드(Price(8_000), MenuType.Appetizer), + 티본스테이크(Price(55_000), MenuType.Main), + 바비큐립(Price(54_000), MenuType.Main), + 해산물파스타(Price(35_000), MenuType.Main), + 크리스마스파스타(Price(25_000), MenuType.Main), + 초코케이크(Price(15_000), MenuType.Dessert), + 아이스크림(Price(5_000), MenuType.Dessert), + 제로콜라(Price(3_000), MenuType.Drink), + 레드와인(Price(60_000), MenuType.Drink), + 샴페인(Price(25_000), MenuType.Drink), + ; + +} + +enum class MenuType { + Appetizer, + Main, + Dessert, + Drink, + ; +} + +data class MenuWithCount(val menu: Menu, val count: Int) { + val benefitAmount = menu.price * count + + override fun toString() = "${this.menu.name} ${this.count}개" +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/model/Price.kt b/kotlin-christmas/src/main/kotlin/christmas/model/Price.kt new file mode 100644 index 0000000..6684944 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/model/Price.kt @@ -0,0 +1,27 @@ +package christmas.model + +import christmas.validation.requireChristmas +import java.text.DecimalFormat + +@JvmInline +value class Price(val value: Int) { + init { + requireChristmas(value >= 0) + } + + operator fun times(other: Int) = Price(this.value * other) + + operator fun plus(other: Price) = Price(this.value + other.value) + + operator fun minus(other: Price) = Price(this.value - other.value) + + operator fun compareTo(other: Price) = this.value.compareTo(other.value) + + operator fun compareTo(other: Int) = this.value.compareTo(other) + + fun toMinusString() = "${DecimalFormat("#,###").format(-value)}원" + + override fun toString() = "${DecimalFormat("#,###").format(value)}원" +} + +fun Int.toPrice() = Price(this) diff --git a/kotlin-christmas/src/main/kotlin/christmas/model/UserBenefitInfo.kt b/kotlin-christmas/src/main/kotlin/christmas/model/UserBenefitInfo.kt new file mode 100644 index 0000000..171160c --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/model/UserBenefitInfo.kt @@ -0,0 +1,25 @@ +package christmas.model + +import christmas.badge.Badge +import christmas.event.EventType + +class UserBenefitInfo( + val benefitList: List, +) { + val giveaway: EventType.Giveaway? = benefitList + .filterIsInstance().firstOrNull() + + val totalBenefitAmount: Price = benefitList.sumOf { + when (it) { + is EventType.Giveaway -> it.menuWithCount.menu.price.value * it.menuWithCount.count + is EventType.Discount -> it.price.value + } + }.toPrice() + + val totalDiscountAmount: Price = benefitList + .filterIsInstance() + .sumOf { it.price.value } + .toPrice() + + val eventBadge = Badge.of(totalBenefitAmount) +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/model/UserOrderInfo.kt b/kotlin-christmas/src/main/kotlin/christmas/model/UserOrderInfo.kt new file mode 100644 index 0000000..6313dfc --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/model/UserOrderInfo.kt @@ -0,0 +1,38 @@ +package christmas.model + +import christmas.menu.MenuType +import christmas.menu.MenuWithCount +import christmas.validation.requireChristmas +import java.time.LocalDate + +private const val MAX_AVAILABLE_MENU_COUNT = 20 + +class UserOrderInfo( + val userOrderMenu: UserOrderMenu, + estimatedVisitDay: VisitDay, +) { + val estimatedVisitDate: LocalDate = LocalDate.of(2023, 12, estimatedVisitDay.day) + val totalPrice: Price = userOrderMenu.getTotalPrice() + + fun getSumPriceOfMenu(menuType: MenuType) = + this.userOrderMenu.menus.sumOf { if (it.menu.menuType == menuType) it.count else 0 } + + @JvmInline + value class VisitDay(val day: Int) { + init { + requireChristmas(day in 1..31) { "유효하지 않은 날짜입니다. 다시 입력해 주세요." } + } + } + + @JvmInline + value class UserOrderMenu(val menus: List) { + init { + requireChristmas(menus.sumOf { it.count } <= MAX_AVAILABLE_MENU_COUNT) { "totalMenuCount는 ${MAX_AVAILABLE_MENU_COUNT}개를 넘을 수 없습니다." } + requireChristmas((menus.all { it.menu.menuType == MenuType.Drink }).not()) { "음료만 주문할 수 없습니다. 주문 메뉴: $menus" } + } + + fun getTotalPrice() = this.menus.sumOf { it.menu.price.value * it.count }.toPrice() + + override fun toString() = menus.joinToString("\n") + } +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/service/ChristmasService.kt b/kotlin-christmas/src/main/kotlin/christmas/service/ChristmasService.kt new file mode 100644 index 0000000..fa73536 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/service/ChristmasService.kt @@ -0,0 +1,22 @@ +package christmas.service + +import christmas.event.EventPolicy +import christmas.event.EventType +import christmas.model.UserBenefitInfo +import christmas.model.UserOrderInfo + +object ChristmasService { + + private val eventPolicyList = EventPolicy.entries + + fun getBenefitInfo(userOrderInfo: UserOrderInfo) = UserBenefitInfo(getBenefitList(userOrderInfo)) + + private fun getBenefitList(userOrderInfo: UserOrderInfo) = + mutableListOf().apply { + for (eventPolicy in eventPolicyList) { + if (eventPolicy.isSupported(userOrderInfo)) { + this.add(eventPolicy.getBenefit(userOrderInfo)) + } + } + }.toList() +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/validation/LottoValidation.kt b/kotlin-christmas/src/main/kotlin/christmas/validation/LottoValidation.kt new file mode 100644 index 0000000..7fba3c8 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/validation/LottoValidation.kt @@ -0,0 +1,17 @@ +package christmas.validation + +const val ERROR_PREFIX = "[ERROR] " + +fun requireChristmas(value: Boolean) { + requireChristmas(value) { "Failed requirement." } +} + +inline fun requireChristmas(value: Boolean, lazyMessage: () -> Any) { + if (!value) { + throw ChristmasException(lazyMessage().toString()) + } +} + +class ChristmasException(message: String) : IllegalArgumentException() { + override val message = ERROR_PREFIX + message +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/view/InputView.kt b/kotlin-christmas/src/main/kotlin/christmas/view/InputView.kt new file mode 100644 index 0000000..e4f0388 --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/view/InputView.kt @@ -0,0 +1,61 @@ +package christmas.view + +import camp.nextstep.edu.missionutils.Console +import christmas.menu.Menu +import christmas.menu.MenuWithCount +import christmas.model.UserOrderInfo +import christmas.model.UserOrderInfo.UserOrderMenu +import christmas.model.UserOrderInfo.VisitDay +import christmas.validation.ChristmasException + +object InputView { + fun getUserOrderInfo(): UserOrderInfo { + val visitDay = tryGetUserInput({ println("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)") }) + { Console.readLine().toVisitDay() } + val userOrderMenu = tryGetUserInput({ println("주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)") }) + { Console.readLine().toUserOrderMenu() } + return UserOrderInfo(userOrderMenu, visitDay) + } +} + +fun String.toVisitDay() = + try { + VisitDay(this.toInt()) + } catch (e: ChristmasException) { + throw e + } catch (e: Exception) { + throw ChristmasException("유효하지 않은 날짜입니다. 다시 입력해 주세요.") + } + +fun String.toUserOrderMenu() = + try { + val userOrderMenu = mutableListOf() + for (menuAndCount in this.split(",")) { + val (menuString, countString) = menuAndCount.split("-") + val menu = Menu.valueOf(menuString) + val count = countString.toInt() + val menuWithCount = MenuWithCount(menu, count) + if (userOrderMenu.contains(menuWithCount)) { + throw IllegalArgumentException() + } + userOrderMenu.add(menuWithCount) + } + UserOrderMenu(userOrderMenu) + } catch (e: ChristmasException) { + throw e + } catch (e: Exception) { + throw ChristmasException("유효하지 않은 주문입니다. 다시 입력해 주세요.") + } + +private fun tryGetUserInput(guideMessage: (() -> Unit)? = null, inputFunction: () -> T): T { + while (true) { + if (guideMessage != null) { + guideMessage() + } + try { + return inputFunction() + } catch (e: IllegalArgumentException) { + println(e.message) + } + } +} diff --git a/kotlin-christmas/src/main/kotlin/christmas/view/OutputView.kt b/kotlin-christmas/src/main/kotlin/christmas/view/OutputView.kt new file mode 100644 index 0000000..162257e --- /dev/null +++ b/kotlin-christmas/src/main/kotlin/christmas/view/OutputView.kt @@ -0,0 +1,42 @@ +package christmas.view + +import christmas.event.EventType +import christmas.model.UserBenefitInfo +import christmas.model.UserOrderInfo + +object OutputView { + fun printWelcomeMessage() { + println("안녕하세요! 우테코 식당 12월 이벤트 플래너입니다.") + } + + fun printUserEventBenefit(userOrderInfo: UserOrderInfo, benefitInfo: UserBenefitInfo) { + val month = userOrderInfo.estimatedVisitDate.month.value + val day = userOrderInfo.estimatedVisitDate.dayOfMonth + println( + """ + |${month}월 ${day}일에 우테코 식당에서 받을 이벤트 혜택 미리 보기! + | + |<주문 메뉴> + |${userOrderInfo.userOrderMenu} + | + |<할인 전 총주문 금액> + |${userOrderInfo.totalPrice} + | + |<증정 메뉴> + |${benefitInfo.giveaway?.menuWithCount ?: "없음"} + | + |<혜택 내역> + |${benefitInfo.benefitList.joinToString("\n").ifEmpty { "없음" }} + | + |<총혜택 금액> + |${benefitInfo.totalBenefitAmount.toMinusString()} + | + |<할인 후 예상 결제 금액> + |${userOrderInfo.totalPrice - benefitInfo.totalDiscountAmount} + | + |<12월 이벤트 배지> + |${benefitInfo.eventBadge ?: "없음"} + """.trimMargin() + ) + } +} From 935bc54c77e8fcd8934e5035fbbc02832d34f0e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=95=B8=EB=AA=A8?= Date: Sun, 25 Feb 2024 01:11:41 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(mockito-kotlin=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kotlin-christmas/build.gradle.kts | 4 + .../kotlin/christmas/event/EventPolicy.kt | 4 +- .../test/kotlin/christmas/badge/BadgeTest.kt | 20 ++++ .../kotlin/christmas/event/EventPolicyTest.kt | 102 ++++++++++++++++++ .../test/kotlin/christmas/model/PriceTest.kt | 45 ++++++++ .../christmas/model/UserBenefitInfoTest.kt | 65 +++++++++++ .../christmas/model/UserOrderInfoTest.kt | 84 +++++++++++++++ .../test/kotlin/christmas/util/TestUtils.kt | 6 ++ 8 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 kotlin-christmas/src/test/kotlin/christmas/badge/BadgeTest.kt create mode 100644 kotlin-christmas/src/test/kotlin/christmas/event/EventPolicyTest.kt create mode 100644 kotlin-christmas/src/test/kotlin/christmas/model/PriceTest.kt create mode 100644 kotlin-christmas/src/test/kotlin/christmas/model/UserBenefitInfoTest.kt create mode 100644 kotlin-christmas/src/test/kotlin/christmas/model/UserOrderInfoTest.kt create mode 100644 kotlin-christmas/src/test/kotlin/christmas/util/TestUtils.kt diff --git a/kotlin-christmas/build.gradle.kts b/kotlin-christmas/build.gradle.kts index c006c73..e8883a3 100644 --- a/kotlin-christmas/build.gradle.kts +++ b/kotlin-christmas/build.gradle.kts @@ -9,6 +9,10 @@ repositories { dependencies { implementation("com.github.woowacourse-projects:mission-utils:1.1.0") + /** + * mockito를 사용하면 any! 타입이 반환되어 NPE가 터짐. 모킹을 너무 사용하고 싶기 때문에 라이브러리 추가... + */ + testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1") } java { diff --git a/kotlin-christmas/src/main/kotlin/christmas/event/EventPolicy.kt b/kotlin-christmas/src/main/kotlin/christmas/event/EventPolicy.kt index 3494482..f078aa6 100644 --- a/kotlin-christmas/src/main/kotlin/christmas/event/EventPolicy.kt +++ b/kotlin-christmas/src/main/kotlin/christmas/event/EventPolicy.kt @@ -5,6 +5,7 @@ import christmas.menu.MenuType import christmas.menu.MenuWithCount import christmas.model.Price import christmas.model.UserOrderInfo +import org.assertj.core.util.VisibleForTesting import java.time.DayOfWeek import java.time.LocalDate import java.time.Month @@ -101,7 +102,8 @@ enum class EventPolicy { private fun isInEventDate(userOrderInfo: UserOrderInfo) = userOrderInfo.estimatedVisitDate.year == 2023 && userOrderInfo.estimatedVisitDate.month == Month.DECEMBER - protected abstract fun isOwnSupported(userOrderInfo: UserOrderInfo): Boolean + @VisibleForTesting + internal abstract fun isOwnSupported(userOrderInfo: UserOrderInfo): Boolean abstract fun getBenefit(userOrderInfo: UserOrderInfo): EventType diff --git a/kotlin-christmas/src/test/kotlin/christmas/badge/BadgeTest.kt b/kotlin-christmas/src/test/kotlin/christmas/badge/BadgeTest.kt new file mode 100644 index 0000000..8833fd3 --- /dev/null +++ b/kotlin-christmas/src/test/kotlin/christmas/badge/BadgeTest.kt @@ -0,0 +1,20 @@ +package christmas.badge + +import christmas.model.Price +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class BadgeTest { + @Test + fun `가격에 맞는 뱃지가 나와야 한다`() { + assertThat(Badge.of(Price(0))).isEqualTo(null) + assertThat(Badge.of(Price(3000))).isEqualTo(null) + assertThat(Badge.of(Price(5000))).isEqualTo(Badge.별) + assertThat(Badge.of(Price(8000))).isEqualTo(Badge.별) + assertThat(Badge.of(Price(10000))).isEqualTo(Badge.트리) + assertThat(Badge.of(Price(15000))).isEqualTo(Badge.트리) + assertThat(Badge.of(Price(20000))).isEqualTo(Badge.산타) + assertThat(Badge.of(Price(20001))).isEqualTo(Badge.산타) + assertThat(Badge.of(Price(1_000_000))).isEqualTo(Badge.산타) + } +} diff --git a/kotlin-christmas/src/test/kotlin/christmas/event/EventPolicyTest.kt b/kotlin-christmas/src/test/kotlin/christmas/event/EventPolicyTest.kt new file mode 100644 index 0000000..924ba92 --- /dev/null +++ b/kotlin-christmas/src/test/kotlin/christmas/event/EventPolicyTest.kt @@ -0,0 +1,102 @@ +package christmas.event + +import christmas.model.Price +import christmas.model.UserOrderInfo +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import java.time.LocalDate + +class EventPolicyTest { + + @Nested + inner class GiveawayEventPolicyTest { + + val giveawayEventPolicy = EventPolicy.GiveawayEventPolicy + + @Test + fun `증정 이벤트는 총 금액이 120,000원 이상이면 제공한다`() { + val userOrderInfo1 = mock { + on { totalPrice } doReturn Price(0) + } + assertThat(giveawayEventPolicy.isOwnSupported(userOrderInfo1)).isFalse + + val userOrderInfo2 = mock { + on { totalPrice } doReturn Price(119_999) + } + assertThat(giveawayEventPolicy.isOwnSupported(userOrderInfo2)).isFalse + + val userOrderInfo3 = mock { + on { totalPrice } doReturn Price(120_000) + } + assertThat(giveawayEventPolicy.isOwnSupported(userOrderInfo3)).isTrue + + val userOrderInfo4 = mock { + on { totalPrice } doReturn Price(200_000) + } + assertThat(giveawayEventPolicy.isOwnSupported(userOrderInfo4)).isTrue + } + } + + @Nested + inner class ChristmasDiscountPolicyTest { + + val christmasDiscountPolicy = EventPolicy.ChristmasDiscountPolicy + + @Test + fun `크리스마스 디데이 할인은 12월 25일 이전이면 제공한다`() { + val userOrderInfo1 = mock { + on { estimatedVisitDate } doReturn LocalDate.of(2023, 12, 1) + } + assertThat(christmasDiscountPolicy.isOwnSupported(userOrderInfo1)).isTrue + + val userOrderInfo2 = mock { + on { estimatedVisitDate } doReturn LocalDate.of(2023, 12, 25) + } + assertThat(christmasDiscountPolicy.isOwnSupported(userOrderInfo2)).isTrue + + val userOrderInfo3 = mock { + on { estimatedVisitDate } doReturn LocalDate.of(2023, 12, 26) + } + assertThat(christmasDiscountPolicy.isOwnSupported(userOrderInfo3)).isFalse + + val userOrderInfo4 = mock { + on { estimatedVisitDate } doReturn LocalDate.of(2023, 12, 31) + } + assertThat(christmasDiscountPolicy.isOwnSupported(userOrderInfo4)).isFalse + } + + @Test + fun `크리스마스 디데이 할인은 하루가 지날 때마다 1000원씩 증가한 할인을 제공한다`() { + val userOrderInfo1 = mock { + on { estimatedVisitDate } doReturn LocalDate.of(2023, 12, 1) + } + assertThat(christmasDiscountPolicy.getBenefit(userOrderInfo1)).isInstanceOf(EventType.Discount::class.java) + assertThat((christmasDiscountPolicy.getBenefit(userOrderInfo1) as EventType.Discount).price) + .isEqualTo(Price(1000)) + + val userOrderInfo2 = mock { + on { estimatedVisitDate } doReturn LocalDate.of(2023, 12, 25) + } + assertThat(christmasDiscountPolicy.getBenefit(userOrderInfo2)).isInstanceOf(EventType.Discount::class.java) + assertThat((christmasDiscountPolicy.getBenefit(userOrderInfo2) as EventType.Discount).price) + .isEqualTo(Price(3400)) + } + } + + // 중략 ... + @Nested + inner class WeekdayDiscountPolicyTest { + } + + @Nested + inner class WeekendDiscountPolicyTest { + } + + @Nested + inner class SpecialDiscountPolicyTest { + } + +} diff --git a/kotlin-christmas/src/test/kotlin/christmas/model/PriceTest.kt b/kotlin-christmas/src/test/kotlin/christmas/model/PriceTest.kt new file mode 100644 index 0000000..2f6adad --- /dev/null +++ b/kotlin-christmas/src/test/kotlin/christmas/model/PriceTest.kt @@ -0,0 +1,45 @@ +package christmas.model + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class PriceTest { + + @Test + fun times() { + assertThat(Price(100) * 10).isEqualTo(Price(1000)) + assertThat(Price(250) * 10).isEqualTo(Price(2500)) + } + + @Test + fun plus() { + assertThat(Price(100) + Price(10)).isEqualTo(Price(110)) + assertThat(Price(250) + Price(10)).isEqualTo(Price(260)) + } + + @Test + fun minus() { + assertThat(Price(100) - Price(10)).isEqualTo(Price(90)) + assertThat(Price(250) - Price(10)).isEqualTo(Price(240)) + } + + @Test + fun compareTo() { + assertThat(Price(100) > Price(10)).isTrue + assertThat(Price(250) < Price(10)).isFalse + assertThat(Price(100) == Price(100)).isTrue + assertThat(Price(100) == Price(50)).isFalse + + assertThat(Price(100) > 10).isTrue + assertThat(Price(250) < 10).isFalse + assertThat(Price(100) >= 100).isTrue + assertThat(Price(100) <= 50).isFalse + } + + @Test + fun `toMinusString을 사용하면 0원을 제외한 나머지 금액은 마이너스가 붙어야 한다`() { + assertThat(Price(0).toMinusString()).isEqualTo("0원") + assertThat(Price(100).toMinusString()).isEqualTo("-100원") + assertThat(Price(12345).toMinusString()).isEqualTo("-12,345원") + } +} diff --git a/kotlin-christmas/src/test/kotlin/christmas/model/UserBenefitInfoTest.kt b/kotlin-christmas/src/test/kotlin/christmas/model/UserBenefitInfoTest.kt new file mode 100644 index 0000000..5b8d7cb --- /dev/null +++ b/kotlin-christmas/src/test/kotlin/christmas/model/UserBenefitInfoTest.kt @@ -0,0 +1,65 @@ +package christmas.model + +import christmas.event.EventType +import christmas.menu.Menu +import christmas.menu.MenuWithCount +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.anyString +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +class UserBenefitInfoTest { + + @Test + fun getGiveawayTest() { + val userBenefitInfo1 = UserBenefitInfo( + listOf( + EventType.Giveaway(MenuWithCount(Menu.양송이스프, 1), anyString()), + EventType.Discount(Price(1000), "anyString"), // stub for testing + ) + ) + assertThat(userBenefitInfo1.giveaway).isNotNull + assertThat(userBenefitInfo1.giveaway!!.menuWithCount).isEqualTo(MenuWithCount(Menu.양송이스프, 1)) + + val userBenefitInfo2 = UserBenefitInfo( + listOf( + EventType.Discount(Price(1000), "anyString"), // stub for testing + ) + ) + assertThat(userBenefitInfo2.giveaway).isNull() + } + + @Test + fun getTotalBenefitAmountTest() { + val menu = mock { + on { price } doReturn Price(1000) + } + val userBenefitInfo = UserBenefitInfo( + listOf( + EventType.Giveaway(MenuWithCount(menu, 3), "anyString"), // 1000 * 3 == 3000원 + EventType.Discount(Price(500), "anyString"), + EventType.Discount(Price(70), "anyString"), + ) + ) + + assertThat(userBenefitInfo.totalBenefitAmount).isEqualTo(Price(3570)) + } + + @Test + fun getTotalDiscountAmountTest() { + + val menu = mock { + on { price } doReturn Price(1000) + } + val userBenefitInfo = UserBenefitInfo( + listOf( + EventType.Giveaway(MenuWithCount(menu, 3), "anyString"), + EventType.Discount(Price(500), "anyString"), + EventType.Discount(Price(70), "anyString"), + ) + ) + + assertThat(userBenefitInfo.totalDiscountAmount).isEqualTo(Price(570)) + } +} diff --git a/kotlin-christmas/src/test/kotlin/christmas/model/UserOrderInfoTest.kt b/kotlin-christmas/src/test/kotlin/christmas/model/UserOrderInfoTest.kt new file mode 100644 index 0000000..c67028b --- /dev/null +++ b/kotlin-christmas/src/test/kotlin/christmas/model/UserOrderInfoTest.kt @@ -0,0 +1,84 @@ +package christmas.model + +import christmas.menu.Menu +import christmas.menu.MenuType +import christmas.menu.MenuWithCount +import christmas.util.assertContains +import christmas.validation.ChristmasException +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows + +class UserOrderInfoTest { + @Nested + inner class VisitDayTest { + @Test + fun `visitday는 1일 ~ 31일 안이어야 한다`() { + assertThrows { UserOrderInfo.VisitDay(0) } + assertDoesNotThrow { UserOrderInfo.VisitDay(1) } + assertDoesNotThrow { UserOrderInfo.VisitDay(31) } + assertThrows { UserOrderInfo.VisitDay(32) } + } + } + + @Nested + inner class UserOrderMenuTest { + @Test + fun `userOrderMenu 개수는 총 20개를 넘으면 안된다`() { + assertDoesNotThrow { + UserOrderInfo.UserOrderMenu(listOf(generateWithoutDrink(1))) + } + assertDoesNotThrow { + UserOrderInfo.UserOrderMenu(listOf(generateWithoutDrink(20))) // 20개 + } + assertDoesNotThrow { + UserOrderInfo.UserOrderMenu((1..19).map { generateWithoutDrink(1) }) // 19개 + } + assertDoesNotThrow { + UserOrderInfo.UserOrderMenu((1..10).map { generateWithoutDrink(2) }) // 20개 + } + assertDoesNotThrow { + UserOrderInfo.UserOrderMenu((1..4).map { generateWithoutDrink(5) }) // 20개 + } + + assertThrows("totalMenuCount는 20개를 넘을 수 없습니다.") { + UserOrderInfo.UserOrderMenu((1..21).map { generateWithoutDrink(1) }) // 21개 + } + assertThrows("totalMenuCount는 20개를 넘을 수 없습니다.") { + UserOrderInfo.UserOrderMenu((1..11).map { generateWithoutDrink(2) }) // 22개 + } + assertThrows("totalMenuCount는 20개를 넘을 수 없습니다.") { + UserOrderInfo.UserOrderMenu((1..8).map { generateWithoutDrink(3) }) // 24개 + } + assertThrows("totalMenuCount는 20개를 넘을 수 없습니다.") { + UserOrderInfo.UserOrderMenu(listOf(generateWithoutDrink(21))) // 21개 + } + } + + @Test + fun `음료만 주문하면 안된다`() { + assertDoesNotThrow { + UserOrderInfo.UserOrderMenu(listOf(generateWithoutDrink())) + } + assertDoesNotThrow { + UserOrderInfo.UserOrderMenu(listOf(generateWithoutDrink(), generateDrink())) + } + + for (i in 1..19) { + assertThrows { + UserOrderInfo.UserOrderMenu((1..i).map { generateDrink() }) + }.message.assertContains("음료만 주문할 수 없습니다.") + } + } + + /** + * 이것도 모킹하면 더 좋지만 체력 이슈로 패스 + */ + private fun generateWithoutDrink(count: Int = 1) = + MenuWithCount(Menu.entries.filter { it.menuType != MenuType.Drink }.random(), count) + + private fun generateDrink() = MenuWithCount(Menu.entries.filter { it.menuType == MenuType.Drink }.random(), 1) + + } +} diff --git a/kotlin-christmas/src/test/kotlin/christmas/util/TestUtils.kt b/kotlin-christmas/src/test/kotlin/christmas/util/TestUtils.kt new file mode 100644 index 0000000..a5cb207 --- /dev/null +++ b/kotlin-christmas/src/test/kotlin/christmas/util/TestUtils.kt @@ -0,0 +1,6 @@ +package christmas.util + +import org.assertj.core.api.AbstractStringAssert +import org.assertj.core.api.Assertions + +fun String.assertContains(message: String): AbstractStringAssert<*> = Assertions.assertThat(this).contains(message)