test: add unit test infrastructure and core-logic tests
First tests in the project (src/test was absent). Add testRuntimeOnly junit-platform-launcher (required by Gradle 9 to load the JUnit Platform), then cover: - VideoMetrics: duration parse, isShorts boundary, viewsPerHour clamp/ division, viewsPerSubRatio rounding & zero-guards (9 cases, pure) - DashboardService.summary(): composes each domain service under the right key (Mockito) - PublishService.dashboardSummary(): status counts + recent capped at 5 12 tests, all green. No Spring context / DB needed — fast. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9828918b97
commit
2cdae03998
@ -37,6 +37,7 @@ dependencies {
|
|||||||
runtimeOnly 'org.postgresql:postgresql'
|
runtimeOnly 'org.postgresql:postgresql'
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
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 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0'
|
||||||
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.2'
|
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.2'
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<PublishPackage> seven = Collections.nCopies(7, mock(PublishPackage.class));
|
||||||
|
when(repository.findAllSorted(any(Sort.class))).thenReturn(seven);
|
||||||
|
|
||||||
|
Map<String, Object> summary = publishService.dashboardSummary();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Long> byStatus = (Map<String, Long>) summary.get("byStatus");
|
||||||
|
assertThat(byStatus)
|
||||||
|
.containsEntry("DRAFT", 2L)
|
||||||
|
.containsEntry("READY", 3L)
|
||||||
|
.containsEntry("PUBLISHED", 4L);
|
||||||
|
|
||||||
|
assertThat(summary.get("total")).isEqualTo(7L);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<PublishPackage> recent = (List<PublishPackage>) 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<String, Object> summary = publishService.dashboardSummary();
|
||||||
|
|
||||||
|
assertThat(summary.get("total")).isEqualTo(3L);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<PublishPackage> recent = (List<PublishPackage>) summary.get("recent");
|
||||||
|
assertThat(recent).hasSize(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<String, Object> pipeline = Map.of("total", 200L);
|
||||||
|
Map<String, Object> categories = Map.of("uncategorized", 200L);
|
||||||
|
Map<String, Object> publish = Map.of("total", 0L);
|
||||||
|
List<ChannelVideo> 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<String, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user