diff --git a/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt b/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt index 4cecc9d1..b576c26b 100644 --- a/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt +++ b/src/test/kotlin/roomescape/theme/business/ThemeServiceTest.kt @@ -1,87 +1,108 @@ package roomescape.theme.business import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldNotThrow import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import org.springframework.data.repository.findByIdOrNull +import io.mockk.* import roomescape.theme.exception.ThemeErrorCode import roomescape.theme.exception.ThemeException +import roomescape.theme.implement.ThemeFinder +import roomescape.theme.implement.ThemeWriter 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.theme.web.ThemeCreateResponse import roomescape.util.ThemeFixture +import java.time.LocalDate class ThemeServiceTest : FunSpec({ + val themeFinder: ThemeFinder = mockk() + val themeWriter: ThemeWriter = mockk() - val themeRepository: ThemeRepository = mockk() - val themeService = ThemeService(TsidFactory, themeRepository) + val themeService = ThemeService(themeFinder, themeWriter) context("findThemeById") { val themeId = 1L - test("조회 성공") { + + test("정상 응답") { val theme: ThemeEntity = ThemeFixture.create(id = themeId) every { - themeRepository.findByIdOrNull(themeId) + themeFinder.findById(themeId) } returns theme theme.id shouldBe themeId } - test("ID로 테마를 찾을 수 없으면 400 예외를 던진다.") { + test("테마를 찾을 수 없으면 예외 응답") { every { - themeRepository.findByIdOrNull(themeId) - } returns null + themeFinder.findById(themeId) + } throws ThemeException(ThemeErrorCode.THEME_NOT_FOUND) - val exception = shouldThrow { + shouldThrow { themeService.findById(themeId) + }.also { + it.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND } - - exception.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND } } - context("findAllThemes") { - test("모든 테마를 조회한다.") { - val themes = listOf(ThemeFixture.create(id = 1, name = "t1"), ThemeFixture.create(id = 2, name = "t2")) + context("findThemes") { + test("정상 응답") { + val themes = listOf( + ThemeFixture.create(id = 1, name = "t1"), + ThemeFixture.create(id = 2, name = "t2") + ) + every { - themeRepository.findAll() + themeFinder.findAll() } returns themes - assertSoftly(themeService.findThemes()) { - this.themes.size shouldBe themes.size - this.themes[0].name shouldBe "t1" - this.themes[1].name shouldBe "t2" + val response = themeService.findThemes() + + assertSoftly(response.themes) { + this.size shouldBe themes.size + it.map { theme -> theme.name } shouldContainExactly themes.map { theme -> theme.name } } } } - context("save") { + context("findMostReservedThemes") { + test("7일 전 부터 1일 전 까지 조회") { + val count = 10 + val startFrom = slot() + val endAt = slot() + + every { + themeFinder.findMostReservedThemes(count, capture(startFrom), capture(endAt)) + } returns emptyList() + + themeService.findMostReservedThemes(count) + + startFrom.captured shouldBe LocalDate.now().minusDays(7) + endAt.captured shouldBe LocalDate.now().minusDays(1) + } + } + + context("createTheme") { val request = ThemeCreateRequest( name = "New Theme", description = "Description", thumbnail = "http://example.com/thumbnail.jpg" ) - test("저장 성공") { + test("정상 저장") { every { - themeRepository.existsByName(request.name) - } returns false - - every { - themeRepository.save(any()) + themeWriter.create(request.name, request.description, request.thumbnail) } returns ThemeFixture.create( - id = 1L, + id = 1, name = request.name, description = request.description, thumbnail = request.thumbnail ) - val response: ThemeRetrieveResponse = themeService.createTheme(request) + val response: ThemeCreateResponse = themeService.createTheme(request) assertSoftly(response) { this.id shouldBe 1L @@ -91,32 +112,51 @@ class ThemeServiceTest : FunSpec({ } } - test("테마 이름이 중복되면 409 예외를 던진다.") { + test("중복된 이름이 있으면 예외 응답") { every { - themeRepository.existsByName(request.name) - } returns true + themeWriter.create(request.name, request.description, request.thumbnail) + } throws ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED) - val exception = shouldThrow { + shouldThrow { themeService.createTheme(request) + }.also { + it.errorCode shouldBe ThemeErrorCode.THEME_NAME_DUPLICATED } - - exception.errorCode shouldBe ThemeErrorCode.THEME_NAME_DUPLICATED } } context("deleteById") { - test("이미 예약 중인 테마라면 409 예외를 던진다.") { - val themeId = 1L + val themeId = 1L + val theme: ThemeEntity = ThemeFixture.create(id = themeId) - every { - themeRepository.isReservedTheme(themeId) - } returns true + test("정상 응답") { + every { themeFinder.findById(themeId) } returns theme + every { themeWriter.delete(theme) } just Runs - val exception = shouldThrow { + shouldNotThrow { themeService.deleteTheme(themeId) } + } - exception.errorCode shouldBe ThemeErrorCode.THEME_ALREADY_RESERVED + test("테마를 찾을 수 없으면 예외 응답") { + every { themeFinder.findById(themeId) } throws ThemeException(ThemeErrorCode.THEME_NOT_FOUND) + + shouldThrow { + themeService.deleteTheme(themeId) + }.also { + it.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND + } + } + + test("예약이 있는 테마이면 예외 응답") { + every { themeFinder.findById(themeId) } returns theme + every { themeWriter.delete(theme) } throws ThemeException(ThemeErrorCode.THEME_ALREADY_RESERVED) + + shouldThrow { + themeService.deleteTheme(themeId) + }.also { + it.errorCode shouldBe ThemeErrorCode.THEME_ALREADY_RESERVED + } } } }) diff --git a/src/test/kotlin/roomescape/theme/implement/ThemeFinderTest.kt b/src/test/kotlin/roomescape/theme/implement/ThemeFinderTest.kt new file mode 100644 index 00000000..c41ef15a --- /dev/null +++ b/src/test/kotlin/roomescape/theme/implement/ThemeFinderTest.kt @@ -0,0 +1,76 @@ +package roomescape.theme.implement + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.springframework.data.repository.findByIdOrNull +import roomescape.theme.exception.ThemeErrorCode +import roomescape.theme.exception.ThemeException +import roomescape.theme.infrastructure.persistence.ThemeRepository +import roomescape.util.ThemeFixture +import java.time.LocalDate + +class ThemeFinderTest : FunSpec({ + val themeRepository: ThemeRepository = mockk() + + val themeFinder = ThemeFinder(themeRepository) + + context("findAll") { + test("모든 테마를 조회한다.") { + every { + themeRepository.findAll() + } returns listOf(mockk(), mockk(), mockk()) + + themeRepository.findAll() shouldHaveSize 3 + } + } + + context("findById") { + val timeId = 1L + test("동일한 ID인 테마를 찾아 응답한다.") { + every { + themeRepository.findByIdOrNull(timeId) + } returns mockk() + + themeFinder.findById(timeId) + + verify(exactly = 1) { + themeRepository.findByIdOrNull(timeId) + } + } + + test("동일한 ID인 테마가 없으면 실패한다.") { + every { + themeRepository.findByIdOrNull(timeId) + } returns null + + shouldThrow { + themeFinder.findById(timeId) + }.also { + it.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND + } + } + } + + context("findMostReservedThemes") { + test("입력된 개수보다 조회된 개수가 작으면 조회된 개수만큼 반환한다.") { + val count = 10 + val startFrom = LocalDate.now().minusDays(7) + val endAt = LocalDate.now().minusDays(1) + + every { + themeRepository.findPopularThemes(startFrom, endAt, count) + } returns listOf( + ThemeFixture.create(id = 1, name = "name1"), + ThemeFixture.create(id = 2, name = "name2"), + ThemeFixture.create(id = 3, name = "name3"), + ) + + themeFinder.findMostReservedThemes(count, startFrom, endAt) shouldHaveSize 3 + } + } +}) diff --git a/src/test/kotlin/roomescape/theme/implement/ThemeValidatorTest.kt b/src/test/kotlin/roomescape/theme/implement/ThemeValidatorTest.kt new file mode 100644 index 00000000..175028dd --- /dev/null +++ b/src/test/kotlin/roomescape/theme/implement/ThemeValidatorTest.kt @@ -0,0 +1,79 @@ +package roomescape.theme.implement + +import io.kotest.assertions.throwables.shouldNotThrow +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 roomescape.theme.exception.ThemeErrorCode +import roomescape.theme.exception.ThemeException +import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.theme.infrastructure.persistence.ThemeRepository +import roomescape.util.ThemeFixture + +class ThemeValidatorTest : FunSpec({ + val themeRepository: ThemeRepository = mockk() + + val themeValidator = ThemeValidator(themeRepository) + + context("validateNameAlreadyExists") { + val name = "name" + + test("같은 이름을 가진 테마가 있으면 예외를 던진다.") { + every { + themeRepository.existsByName(name) + } returns true + + shouldThrow { + themeValidator.validateNameAlreadyExists(name) + }.also { + it.errorCode shouldBe ThemeErrorCode.THEME_NAME_DUPLICATED + } + } + + test("같은 이름을 가진 테마가 없으면 종료한다.") { + every { + themeRepository.existsByName(name) + } returns false + + shouldNotThrow { + themeValidator.validateNameAlreadyExists(name) + } + } + } + + context("validateIsReserved") { + test("입력된 id가 null 이면 예외를 던진다.") { + shouldThrow { + themeValidator.validateIsReserved(ThemeFixture.create(id = null)) + }.also { + it.errorCode shouldBe ThemeErrorCode.INVALID_REQUEST_VALUE + } + } + + val theme: ThemeEntity = ThemeFixture.create(id = 1L, name = "name") + + test("예약이 있는 테마이면 예외를 던진다.") { + every { + themeRepository.isReservedTheme(theme.id!!) + } returns true + + shouldThrow { + themeValidator.validateIsReserved(theme) + }.also { + it.errorCode shouldBe ThemeErrorCode.THEME_ALREADY_RESERVED + } + } + + test("예약이 없는 테마이면 종료한다.") { + every { + themeRepository.isReservedTheme(theme.id!!) + } returns false + + shouldNotThrow { + themeValidator.validateIsReserved(theme) + } + } + } +}) diff --git a/src/test/kotlin/roomescape/theme/implement/ThemeWriterTest.kt b/src/test/kotlin/roomescape/theme/implement/ThemeWriterTest.kt new file mode 100644 index 00000000..eacfd714 --- /dev/null +++ b/src/test/kotlin/roomescape/theme/implement/ThemeWriterTest.kt @@ -0,0 +1,86 @@ +package roomescape.theme.implement + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.* +import roomescape.common.config.next +import roomescape.theme.exception.ThemeErrorCode +import roomescape.theme.exception.ThemeException +import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.theme.infrastructure.persistence.ThemeRepository +import roomescape.util.ThemeFixture +import roomescape.util.TsidFactory + +class ThemeWriterTest : FunSpec({ + + val themeValidator: ThemeValidator = mockk() + val themeRepository: ThemeRepository = mockk() + + val themeWriter = ThemeWriter(themeValidator, themeRepository, TsidFactory) + + context("create") { + val name = "name" + val description = "description" + val thumbnail = "thumbnail" + + test("중복된 이름이 있으면 실패한다.") { + every { + themeValidator.validateNameAlreadyExists(name) + } throws ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED) + + shouldThrow { + themeWriter.create(name, description, thumbnail) + }.also { + it.errorCode shouldBe ThemeErrorCode.THEME_NAME_DUPLICATED + } + } + + test("중복된 이름이 없으면 저장한다.") { + every { + themeValidator.validateNameAlreadyExists(name) + } just Runs + + every { + themeRepository.save(any()) + } returns ThemeFixture.create(name = name, description = description, thumbnail = thumbnail) + + themeWriter.create(name, description, thumbnail) + + verify(exactly = 1) { + themeRepository.save(any()) + } + } + } + + context("delete") { + val theme: ThemeEntity = ThemeFixture.create(id = TsidFactory.next()) + test("예약이 있는 테마이면 실패한다.") { + every { + themeValidator.validateIsReserved(theme) + } throws ThemeException(ThemeErrorCode.THEME_ALREADY_RESERVED) + + shouldThrow { + themeWriter.delete(theme) + }.also { + it.errorCode shouldBe ThemeErrorCode.THEME_ALREADY_RESERVED + } + } + + test("예약이 없는 테마이면 제거한다.") { + every { + themeValidator.validateIsReserved(theme) + } just Runs + + every { + themeRepository.delete(theme) + } just Runs + + themeWriter.delete(theme) + + verify(exactly = 1) { + themeRepository.delete(theme) + } + } + } +}) diff --git a/src/test/kotlin/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt b/src/test/kotlin/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt index 2784f4ea..6b8e6aa8 100644 --- a/src/test/kotlin/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt +++ b/src/test/kotlin/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt @@ -5,7 +5,7 @@ import io.kotest.matchers.collections.shouldContainInOrder import io.kotest.matchers.shouldBe import jakarta.persistence.EntityManager import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import roomescape.theme.util.TestThemeCreateUtil +import roomescape.theme.util.TestThemeDataHelper import java.time.LocalDate @DataJpaTest(showSql = false) @@ -14,12 +14,13 @@ class ThemeRepositoryTest( val entityManager: EntityManager ) : FunSpec() { + val helper = TestThemeDataHelper(entityManager, transactionTemplate = null) + init { context("findTopNThemeBetweenStartDateAndEndDate") { beforeTest { for (i in 1..10) { - TestThemeCreateUtil.createThemeWithReservations( - entityManager = entityManager, + helper.createThemeWithReservations( name = "테마$i", reservedCount = i, date = LocalDate.now().minusDays(i.toLong()), @@ -27,7 +28,7 @@ class ThemeRepositoryTest( } } - test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회한다.") { + test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회") { themeRepository.findPopularThemes( LocalDate.now().minusDays(10), LocalDate.now().minusDays(1), @@ -40,7 +41,7 @@ class ThemeRepositoryTest( } } - test("8일 전부터 5일 전까지 예약 수가 가장 많은 테마 3개를 조회한다.") { + test("8일 전부터 5일 전까지 예약 수가 가장 많은 테마 3개를 조회") { themeRepository.findPopularThemes( LocalDate.now().minusDays(8), LocalDate.now().minusDays(5), @@ -53,9 +54,8 @@ class ThemeRepositoryTest( } } - test("예약 수가 동일하면 먼저 생성된 테마를 우선 조회한다.") { - TestThemeCreateUtil.createThemeWithReservations( - entityManager = entityManager, + test("예약 수가 동일하면 먼저 생성된 테마를 우선 조회") { + helper.createThemeWithReservations( name = "테마11", reservedCount = 5, date = LocalDate.now().minusDays(5), @@ -73,7 +73,7 @@ class ThemeRepositoryTest( } } - test("입력된 갯수보다 조회된 갯수가 작으면, 조회된 갯수만큼 반환한다.") { + test("입력된 갯수보다 조회된 갯수가 작으면, 조회된 갯수만큼 반환") { themeRepository.findPopularThemes( LocalDate.now().minusDays(10), LocalDate.now().minusDays(6), @@ -83,7 +83,7 @@ class ThemeRepositoryTest( } } - test("입력된 갯수보다 조회된 갯수가 많으면, 입력된 갯수만큼 반환한다.") { + test("입력된 갯수보다 조회된 갯수가 많으면, 입력된 갯수만큼 반환") { themeRepository.findPopularThemes( LocalDate.now().minusDays(10), LocalDate.now().minusDays(1), @@ -93,7 +93,7 @@ class ThemeRepositoryTest( } } - test("입력된 날짜 범위에 예약된 테마가 없을 경우 빈 리스트를 반환한다.") { + test("입력된 날짜 범위에 예약된 테마가 없을 경우 빈 리스트 반환") { themeRepository.findPopularThemes( LocalDate.now().plusDays(1), LocalDate.now().plusDays(10), @@ -106,26 +106,24 @@ class ThemeRepositoryTest( context("existsByName ") { val themeName = "test-theme" beforeTest { - TestThemeCreateUtil.createThemeWithReservations( - entityManager = entityManager, + helper.createThemeWithReservations( name = themeName, reservedCount = 0, date = LocalDate.now() ) } - test("테마 이름이 존재하면 true를 반환한다.") { + test("테마 이름이 존재하면 true 반환") { themeRepository.existsByName(themeName) shouldBe true } - test("테마 이름이 존재하지 않으면 false를 반환한다.") { + test("테마 이름이 존재하지 않으면 false 반환") { themeRepository.existsByName(themeName.repeat(2)) shouldBe false } } context("isReservedTheme") { - test("테마가 예약 중이면 true를 반환한다.") { - val theme = TestThemeCreateUtil.createThemeWithReservations( - entityManager = entityManager, + test("테마가 예약 중이면 true 반환") { + val theme = helper.createThemeWithReservations( name = "예약된 테마", reservedCount = 1, date = LocalDate.now() @@ -133,9 +131,8 @@ class ThemeRepositoryTest( themeRepository.isReservedTheme(theme.id!!) shouldBe true } - test("테마가 예약 중이 아니면 false를 반환한다.") { - val theme = TestThemeCreateUtil.createThemeWithReservations( - entityManager = entityManager, + test("테마가 예약 중이 아니면 false 반환") { + val theme = helper.createThemeWithReservations( name = "예약되지 않은 테마", reservedCount = 0, date = LocalDate.now() diff --git a/src/test/kotlin/roomescape/theme/web/MostReservedThemeApiTest.kt b/src/test/kotlin/roomescape/theme/web/MostReservedThemeApiTest.kt index 48911cd5..b29a4bba 100644 --- a/src/test/kotlin/roomescape/theme/web/MostReservedThemeApiTest.kt +++ b/src/test/kotlin/roomescape/theme/web/MostReservedThemeApiTest.kt @@ -9,7 +9,7 @@ import org.hamcrest.Matchers.equalTo import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.transaction.support.TransactionTemplate -import roomescape.theme.util.TestThemeCreateUtil +import roomescape.theme.util.TestThemeDataHelper import roomescape.util.CleanerMode import roomescape.util.DatabaseCleanerExtension import java.time.LocalDate @@ -20,16 +20,16 @@ class MostReservedThemeApiTest( @LocalServerPort val port: Int, val transactionTemplate: TransactionTemplate, val entityManager: EntityManager, -) : FunSpec({ - extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_SPEC)) -}) { +) : FunSpec({ extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_SPEC)) }) { + + val helper = TestThemeDataHelper(entityManager, transactionTemplate) + init { beforeSpec { transactionTemplate.executeWithoutResult { - // 지난 7일간 예약된 테마 10개 생성 + // 7일 전 ~ 1일 전 예약된 테마 10개 생성 for (i in 1..10) { - TestThemeCreateUtil.createThemeWithReservations( - entityManager = entityManager, + helper.createThemeWithReservations( name = "테마$i", reservedCount = 1, date = LocalDate.now().minusDays(Random.nextLong(1, 7)) @@ -37,16 +37,22 @@ class MostReservedThemeApiTest( } // 8일 전 예약된 테마 1개 생성 - TestThemeCreateUtil.createThemeWithReservations( - entityManager = entityManager, + helper.createThemeWithReservations( name = "테마11", reservedCount = 1, date = LocalDate.now().minusDays(8) ) + + // 당일 예약된 테마 1개 생성 + helper.createThemeWithReservations( + name = "테마12", + reservedCount = 1, + date = LocalDate.now() + ) } } - context("지난 주 가장 많이 예약된 테마 API") { + context("GET /themes/most-reserved-last-week") { val endpoint = "/themes/most-reserved-last-week" test("count 파라미터가 없으면 10개를 반환한다") { @@ -87,8 +93,8 @@ class MostReservedThemeApiTest( } test("지난 7일 동안의 예약만 집계한다") { - // 8일 전에 예약된 테마는 집계에서 제외되어야 한다. - val count = 11 + // beforeSpec 에서 정의한 테스트 데이터 중, 8일 전 / 당일 예약 테마는 제외되어야 한다. + val count = 12 Given { port(port) param("count", count) diff --git a/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt b/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt index a6cc1dff..8fb1745a 100644 --- a/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt +++ b/src/test/kotlin/roomescape/theme/web/ThemeControllerTest.kt @@ -1,42 +1,32 @@ package roomescape.theme.web import com.ninjasquad.springmockk.MockkBean -import com.ninjasquad.springmockk.SpykBean -import io.kotest.assertions.assertSoftly -import io.kotest.matchers.collections.shouldContainAll -import io.kotest.matchers.shouldBe import io.mockk.every -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 +import roomescape.common.exception.CommonErrorCode import roomescape.theme.business.ThemeService import roomescape.theme.exception.ThemeErrorCode -import roomescape.theme.infrastructure.persistence.ThemeRepository +import roomescape.theme.exception.ThemeException import roomescape.util.RoomescapeApiTest import roomescape.util.ThemeFixture @WebMvcTest(ThemeController::class) -class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { - - @SpykBean +class ThemeControllerTest(val mockMvc: MockMvc) : RoomescapeApiTest() { + @MockkBean private lateinit var themeService: ThemeService - @MockkBean - private lateinit var themeRepository: ThemeRepository - init { - Given("모든 테마를 조회할 때") { + Given("GET /themes 요청을") { val endpoint = "/themes" - When("로그인 상태가 아니라면") { + When("로그인 하지 않은 사용자가 보내면") { doNotLogin() - Then("에러 응답을 받는다.") { + Then("예외 응답") { val expectedError = AuthErrorCode.MEMBER_NOT_FOUND runGetTest( mockMvc = mockMvc, @@ -52,34 +42,32 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { When("로그인 상태라면") { loginAsUser() - Then("조회에 성공한다.") { + Then("정상 응답") { every { - themeRepository.findAll() + themeService.findThemes() } returns listOf( ThemeFixture.create(id = 1, name = "theme1"), ThemeFixture.create(id = 2, name = "theme2"), ThemeFixture.create(id = 3, name = "theme3") - ) + ).toRetrieveListResponse() - val response: ThemeRetrieveListResponse = runGetTest( + runGetTest( mockMvc = mockMvc, endpoint = endpoint, ) { status { isOk() } content { contentType(MediaType.APPLICATION_JSON) + jsonPath("$.data.themes[0].id") { value(1) } + jsonPath("$.data.themes[1].id") { value(2) } + jsonPath("$.data.themes[2].id") { value(3) } } - }.andReturn().readValue(ThemeRetrieveListResponse::class.java) - - assertSoftly(response.themes) { - it.size shouldBe 3 - it.map { m -> m.name } shouldContainAll listOf("theme1", "theme2", "theme3") } } } } - Given("테마를 추가할 때") { + Given("POST /themes 요청을") { val endpoint = "/themes" val request = ThemeCreateRequest( name = "theme1", @@ -87,9 +75,10 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { thumbnail = "http://example.com/thumbnail1.jpg" ) - When("로그인 상태가 아니라면") { + When("로그인 하지 않은 사용자가 보내면") { doNotLogin() - Then("에러 응답을 받는다.") { + + Then("예외 응답") { val expectedError = AuthErrorCode.MEMBER_NOT_FOUND runPostTest( mockMvc = mockMvc, @@ -102,9 +91,10 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { } } - When("관리자가 아닌 회원은") { + When("관리자가 아닌 사용자가 보내면") { loginAsUser() - Then("에러 응답을 받는다.") { + + Then("예외 응답") { val expectedError = AuthErrorCode.ACCESS_DENIED runPostTest( mockMvc = mockMvc, @@ -117,15 +107,17 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { } } - When("동일한 이름의 테마가 있으면") { - loginAsAdmin() + When("관리자가 보낼 때") { + beforeTest { + loginAsAdmin() + } - val expectedError = ThemeErrorCode.THEME_NAME_DUPLICATED + Then("동일한 이름의 테마가 있으면 예외 응답") { + val expectedError = ThemeErrorCode.THEME_NAME_DUPLICATED - Then("에러 응답을 받는다.") { every { - themeRepository.existsByName(request.name) - } returns true + themeService.createTheme(request) + } throws ThemeException(expectedError) runPostTest( mockMvc = mockMvc, @@ -136,80 +128,70 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { jsonPath("$.code") { value(expectedError.errorCode) } } } - } - When("값이 잘못 입력되면 400 에러를 응답한다") { - beforeTest { - loginAsAdmin() - } + When("입력 값의 형식이 잘못되면 예외 응답") { + val request = ThemeCreateRequest( + name = "theme1", + description = "description1", + thumbnail = "http://example.com/thumbnail1.jpg" + ) - val request = ThemeCreateRequest( - name = "theme1", - description = "description1", - thumbnail = "http://example.com/thumbnail1.jpg" - ) + fun runTest(request: ThemeCreateRequest) { + val expectedError = CommonErrorCode.INVALID_INPUT_VALUE - fun runTest(request: ThemeCreateRequest) { - runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = request, - ) { - status { isBadRequest() } + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + ) { + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code") { value(expectedError.errorCode) } + } + } + + Then("이름이 공백인 경우") { + val invalidRequest = request.copy(name = " ") + runTest(invalidRequest) + } + + Then("이름이 20글자를 초과하는 경우") { + val invalidRequest = request.copy(name = "a".repeat(21)) + runTest(invalidRequest) + } + + Then("설명이 공백인 경우") { + val invalidRequest = request.copy(description = " ") + runTest(invalidRequest) + } + + Then("설명이 100글자를 초과하는 경우") { + val invalidRequest = request.copy(description = "a".repeat(101)) + runTest(invalidRequest) + } + + Then("썸네일이 공백인 경우") { + val invalidRequest = request.copy(thumbnail = " ") + runTest(invalidRequest) + } + + Then("썸네일이 URL 형식이 아닌 경우") { + val invalidRequest = request.copy(thumbnail = "invalid-url") + runTest(invalidRequest) } } - Then("이름이 공백인 경우") { - val invalidRequest = request.copy(name = " ") - runTest(invalidRequest) - } + Then("정상 응답") { + val theme = ThemeFixture.create( + id = 1, + name = request.name, + description = request.description, + thumbnail = request.thumbnail + ) - Then("이름이 20글자를 초과하는 경우") { - val invalidRequest = request.copy(name = "a".repeat(21)) - runTest(invalidRequest) - } + every { + themeService.createTheme(request) + } returns theme.toCreateResponse() - Then("설명이 공백인 경우") { - val invalidRequest = request.copy(description = " ") - runTest(invalidRequest) - } - - Then("설명이 100글자를 초과하는 경우") { - val invalidRequest = request.copy(description = "a".repeat(101)) - runTest(invalidRequest) - } - - Then("썸네일이 공백인 경우") { - val invalidRequest = request.copy(thumbnail = " ") - runTest(invalidRequest) - } - - Then("썸네일이 URL 형식이 아닌 경우") { - val invalidRequest = request.copy(thumbnail = "invalid-url") - runTest(invalidRequest) - } - } - - When("저장에 성공하면") { - loginAsAdmin() - - val theme = ThemeFixture.create( - id = 1, - name = request.name, - description = request.description, - thumbnail = request.thumbnail - ) - - every { - themeService.createTheme(request) - } returns ThemeRetrieveResponse( - id = theme.id!!, - name = theme.name, - description = theme.description, - thumbnail = theme.thumbnail - ) - - Then("201 응답을 받는다.") { runPostTest( mockMvc = mockMvc, endpoint = endpoint, @@ -228,13 +210,14 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { } } - Given("테마를 제거할 때") { + Given("DELETE /themes/{id} 요청을") { val themeId = 1L val endpoint = "/themes/$themeId" - When("로그인 상태가 아니라면") { - doNotLogin() - Then("에러 응답을 받는다.") { + When("관리자가 아닌 사용자가 보내면 예외 응답") { + Then("로그인 하지 않은 경우") { + doNotLogin() + val expectedError = AuthErrorCode.MEMBER_NOT_FOUND runDeleteTest( mockMvc = mockMvc, @@ -244,11 +227,10 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { jsonPath("$.code", equalTo(expectedError.errorCode)) } } - } - When("관리자가 아닌 회원은") { - loginAsUser() - Then("에러 응답을 받는다.") { + Then("로그인은 하였으나 관리자가 아닌 경우") { + loginAsUser() + val expectedError = AuthErrorCode.ACCESS_DENIED runDeleteTest( mockMvc = mockMvc, @@ -260,14 +242,16 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { } } - When("이미 예약된 테마이면") { - loginAsAdmin() - val expectedError = ThemeErrorCode.THEME_ALREADY_RESERVED + When("관리자가 보낼 때") { + beforeTest { + loginAsAdmin() + } - Then("에러 응답을 받는다.") { + Then("이미 예약된 테마이면 예외 응답") { + val expectedError = ThemeErrorCode.THEME_ALREADY_RESERVED every { - themeRepository.isReservedTheme(themeId) - } returns true + themeService.deleteTheme(themeId) + } throws ThemeException(expectedError) runDeleteTest( mockMvc = mockMvc, @@ -277,20 +261,10 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { jsonPath("$.code") { value(expectedError.errorCode) } } } - } - When("정상적으로 제거되면") { - loginAsAdmin() + Then("정상 응답") { + every { themeService.deleteTheme(themeId) } returns Unit - every { - themeRepository.isReservedTheme(themeId) - } returns false - - every { - themeRepository.deleteById(themeId) - } just runs - - Then("204 응답을 받는다.") { runDeleteTest( mockMvc = mockMvc, endpoint = endpoint,