diff --git a/build.gradle b/build.gradle index e7749af..2613939 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,7 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0' implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.2' diff --git a/src/test/java/com/hlab/yanalyst/domain/channel/VideoMetricsTest.java b/src/test/java/com/hlab/yanalyst/domain/channel/VideoMetricsTest.java new file mode 100644 index 0000000..aaae36c --- /dev/null +++ b/src/test/java/com/hlab/yanalyst/domain/channel/VideoMetricsTest.java @@ -0,0 +1,75 @@ +package com.hlab.yanalyst.domain.channel; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 순수 지표 계산 유틸 단위 테스트 (DB/Spring 불필요). + */ +class VideoMetricsTest { + + @Test + void parseDurationSec_parsesIso8601() { + assertThat(VideoMetrics.parseDurationSec("PT1M5S")).isEqualTo(65); + assertThat(VideoMetrics.parseDurationSec("PT3M")).isEqualTo(180); + assertThat(VideoMetrics.parseDurationSec("PT1H")).isEqualTo(3600); + } + + @Test + void parseDurationSec_returnsNullForBlankOrInvalid() { + assertThat(VideoMetrics.parseDurationSec(null)).isNull(); + assertThat(VideoMetrics.parseDurationSec("")).isNull(); + assertThat(VideoMetrics.parseDurationSec(" ")).isNull(); + assertThat(VideoMetrics.parseDurationSec("not-a-duration")).isNull(); + } + + @Test + void isShorts_trueWhenAtMost65Seconds() { + assertThat(VideoMetrics.isShorts(60)).isTrue(); + assertThat(VideoMetrics.isShorts(65)).isTrue(); // boundary inclusive + assertThat(VideoMetrics.isShorts(66)).isFalse(); + assertThat(VideoMetrics.isShorts(null)).isFalse(); + } + + @Test + void viewsPerHour_zeroWhenInputMissing() { + assertThat(VideoMetrics.viewsPerHour(null, LocalDateTime.now())).isEqualByComparingTo(BigDecimal.ZERO); + assertThat(VideoMetrics.viewsPerHour(1000L, null)).isEqualByComparingTo(BigDecimal.ZERO); + } + + @Test + void viewsPerHour_clampsElapsedToAtLeastOneHour() { + // published just now -> elapsed < 1h -> treated as 1h -> ratio == viewCount + assertThat(VideoMetrics.viewsPerHour(500L, LocalDateTime.now())) + .isEqualByComparingTo(new BigDecimal("500.00")); + } + + @Test + void viewsPerHour_dividesByElapsedHours() { + assertThat(VideoMetrics.viewsPerHour(1000L, LocalDateTime.now().minusHours(10))) + .isEqualByComparingTo(new BigDecimal("100.00")); + } + + @Test + void viewsPerSubRatio_dividesViewsBySubscribers() { + assertThat(VideoMetrics.viewsPerSubRatio(1000L, 100L)) + .isEqualByComparingTo(new BigDecimal("10.00")); + } + + @Test + void viewsPerSubRatio_roundsHalfUp() { + assertThat(VideoMetrics.viewsPerSubRatio(10L, 3L)) + .isEqualByComparingTo(new BigDecimal("3.33")); + } + + @Test + void viewsPerSubRatio_zeroWhenSubscribersMissingOrZero() { + assertThat(VideoMetrics.viewsPerSubRatio(1000L, 0L)).isEqualByComparingTo(BigDecimal.ZERO); + assertThat(VideoMetrics.viewsPerSubRatio(1000L, null)).isEqualByComparingTo(BigDecimal.ZERO); + assertThat(VideoMetrics.viewsPerSubRatio(null, 100L)).isEqualByComparingTo(BigDecimal.ZERO); + } +} diff --git a/src/test/java/com/hlab/yanalyst/domain/publish/PublishServiceTest.java b/src/test/java/com/hlab/yanalyst/domain/publish/PublishServiceTest.java new file mode 100644 index 0000000..2691b13 --- /dev/null +++ b/src/test/java/com/hlab/yanalyst/domain/publish/PublishServiceTest.java @@ -0,0 +1,67 @@ +package com.hlab.yanalyst.domain.publish; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Sort; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 발행 대시보드 요약: 상태별 카운트 + 최근 5건(전체는 더 많아도 5개로 잘림). + */ +@ExtendWith(MockitoExtension.class) +class PublishServiceTest { + + @Mock PublishPackageRepository repository; + + @InjectMocks PublishService publishService; + + @Test + void dashboardSummary_countsByStatusAndTakesFirstFiveAsRecent() { + when(repository.countByStatus("DRAFT")).thenReturn(2L); + when(repository.countByStatus("READY")).thenReturn(3L); + when(repository.countByStatus("PUBLISHED")).thenReturn(4L); + // list(null) -> repository.findAllSorted(sort); return 7 so recent should cap at 5 + List seven = Collections.nCopies(7, mock(PublishPackage.class)); + when(repository.findAllSorted(any(Sort.class))).thenReturn(seven); + + Map summary = publishService.dashboardSummary(); + + @SuppressWarnings("unchecked") + Map byStatus = (Map) summary.get("byStatus"); + assertThat(byStatus) + .containsEntry("DRAFT", 2L) + .containsEntry("READY", 3L) + .containsEntry("PUBLISHED", 4L); + + assertThat(summary.get("total")).isEqualTo(7L); + + @SuppressWarnings("unchecked") + List recent = (List) summary.get("recent"); + assertThat(recent).hasSize(5); + } + + @Test + void dashboardSummary_recentKeepsAllWhenFiveOrFewer() { + when(repository.countByStatus(any())).thenReturn(0L); + when(repository.findAllSorted(any(Sort.class))) + .thenReturn(Collections.nCopies(3, mock(PublishPackage.class))); + + Map summary = publishService.dashboardSummary(); + + assertThat(summary.get("total")).isEqualTo(3L); + @SuppressWarnings("unchecked") + List recent = (List) summary.get("recent"); + assertThat(recent).hasSize(3); + } +} diff --git a/src/test/java/com/hlab/yanalyst/service/DashboardServiceTest.java b/src/test/java/com/hlab/yanalyst/service/DashboardServiceTest.java new file mode 100644 index 0000000..24024c7 --- /dev/null +++ b/src/test/java/com/hlab/yanalyst/service/DashboardServiceTest.java @@ -0,0 +1,53 @@ +package com.hlab.yanalyst.service; + +import com.hlab.yanalyst.domain.category.CategoryService; +import com.hlab.yanalyst.domain.channel.ChannelVideo; +import com.hlab.yanalyst.domain.channel.ChannelVideoCurationService; +import com.hlab.yanalyst.domain.publish.PublishService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 대시보드 단일 집계가 각 도메인 서비스 결과를 올바른 키로 조합하는지 검증. + */ +@ExtendWith(MockitoExtension.class) +class DashboardServiceTest { + + @Mock ChannelVideoCurationService curationService; + @Mock CategoryService categoryService; + @Mock PublishService publishService; + + @InjectMocks DashboardService dashboardService; + + @Test + void summary_composesEachServiceUnderItsKey() { + Map pipeline = Map.of("total", 200L); + Map categories = Map.of("uncategorized", 200L); + Map publish = Map.of("total", 0L); + List outperformers = List.of(mock(ChannelVideo.class), mock(ChannelVideo.class)); + + when(curationService.pipelineStats()).thenReturn(pipeline); + when(categoryService.distribution()).thenReturn(categories); + when(publishService.dashboardSummary()).thenReturn(publish); + when(curationService.findOutperformers(5, BigDecimal.ONE)).thenReturn(outperformers); + + Map result = dashboardService.summary(); + + assertThat(result).containsOnlyKeys("pipeline", "categories", "publish", "outperformers"); + assertThat(result.get("pipeline")).isSameAs(pipeline); + assertThat(result.get("categories")).isSameAs(categories); + assertThat(result.get("publish")).isSameAs(publish); + assertThat(result.get("outperformers")).isSameAs(outperformers); + } +}