From 5b188e1ac3fbde60307ec9e1fe70ec78818300bc Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Wed, 29 Oct 2025 16:22:15 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[test]=20News=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=ED=86=B5=ED=95=A9/=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../news/controller/NewsControllerTest.java | 119 ++++++++++++++++++ .../news/service/NewsInfoServiceTest.java | 98 +++++++++++++++ .../news/service/NewsLatelyServiceTest.java | 58 +++++++++ 3 files changed, 275 insertions(+) create mode 100644 src/test/java/Konkuk/U2E/domain/news/controller/NewsControllerTest.java create mode 100644 src/test/java/Konkuk/U2E/domain/news/service/NewsInfoServiceTest.java create mode 100644 src/test/java/Konkuk/U2E/domain/news/service/NewsLatelyServiceTest.java diff --git a/src/test/java/Konkuk/U2E/domain/news/controller/NewsControllerTest.java b/src/test/java/Konkuk/U2E/domain/news/controller/NewsControllerTest.java new file mode 100644 index 0000000..c9dbc33 --- /dev/null +++ b/src/test/java/Konkuk/U2E/domain/news/controller/NewsControllerTest.java @@ -0,0 +1,119 @@ +package Konkuk.U2E.domain.news.controller; + +import Konkuk.U2E.domain.news.dto.response.GetLatelyNewsResponse; +import Konkuk.U2E.domain.news.dto.response.GetNewsInfoResponse; +import Konkuk.U2E.domain.news.dto.response.LatelyNews; +import Konkuk.U2E.domain.news.service.NewsInfoService; +import Konkuk.U2E.domain.news.service.NewsLatelyService; +import Konkuk.U2E.global.openApi.gemini.dto.response.RelatedArticle; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +class NewsControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockitoBean + NewsLatelyService newsLatelyService; + + @MockitoBean + NewsInfoService newsInfoService; + + @Nested + @DisplayName("GET /news/lately") + class LatelyApi { + + @Test + @DisplayName("최신 뉴스 목록을 BaseResponse(result)로 감싸서 반환한다") + void viewLatelyNewsList() throws Exception { + // given + LatelyNews n1 = new LatelyNews( + 1L, + List.of("Seoul", "Busan"), // regionList + List.of(), // climateList (enum 리스트, 여기선 빈 리스트) + "제목1" + ); + LatelyNews n2 = new LatelyNews( + 2L, + List.of("Incheon"), + List.of(), + "제목2" + ); + GetLatelyNewsResponse response = GetLatelyNewsResponse.of(List.of(n1, n2)); + + Mockito.when(newsLatelyService.getLatelyNews()).thenReturn(response); + + // when & then + mockMvc.perform(get("/news/lately").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + // BaseResponse 가 {"result": {...}} 라는 전제 + .andExpect(jsonPath("$.data.latelyNewsList", hasSize(2))) + .andExpect(jsonPath("$.data.latelyNewsList[0].newsId").value(1L)) + .andExpect(jsonPath("$.data.latelyNewsList[0].newsTitle").value("제목1")) + .andExpect(jsonPath("$.data.latelyNewsList[0].regionList", hasSize(2))) + .andExpect(jsonPath("$.data.latelyNewsList[0].regionList[0]").value("Seoul")) + .andExpect(jsonPath("$.data.latelyNewsList[1].newsId").value(2L)) + .andExpect(jsonPath("$.data.latelyNewsList[1].regionList[0]").value("Incheon")); + } + } + + @Nested + @DisplayName("GET /news/{newsId}") + class InfoApi { + + @Test + @DisplayName("뉴스 상세를 BaseResponse(result)로 감싸서 반환한다") + void viewNewsInfo() throws Exception { + // given + GetNewsInfoResponse dto = new GetNewsInfoResponse( + List.of(), // climateList + List.of("Seoul", "Busan"), // regionList + "테스트 제목", // newsTitle + "https://example.com/news", // newsUrl + "https://example.com/img.jpg", // newsImageUrl + "요약 본문", // newsBody (aiSummary 매핑) + "2025-10-29", // newsDate + "AI 솔루션 본문", // aiSolution + List.of(new RelatedArticle("관련1", "https://a.com")) + ); + + Mockito.when(newsInfoService.getNewsInfo(anyLong())).thenReturn(dto); + + // when & then + mockMvc.perform(get("/news/{newsId}", 10L).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.data.newsTitle").value("테스트 제목")) + .andExpect(jsonPath("$.data.newsUrl").value("https://example.com/news")) + .andExpect(jsonPath("$.data.newsImageUrl").value("https://example.com/img.jpg")) + .andExpect(jsonPath("$.data.newsBody").value("요약 본문")) + .andExpect(jsonPath("$.data.newsDate").value("2025-10-29")) + .andExpect(jsonPath("$.data.regionList", hasSize(2))) + .andExpect(jsonPath("$.data.aiSolution").value("AI 솔루션 본문")) + .andExpect(jsonPath("$.data.aiRelated", hasSize(1))) + .andExpect(jsonPath("$.data.aiRelated[0].title").value("관련1")) + .andExpect(jsonPath("$.data.aiRelated[0].url").value("https://a.com")); + } + } +} \ No newline at end of file diff --git a/src/test/java/Konkuk/U2E/domain/news/service/NewsInfoServiceTest.java b/src/test/java/Konkuk/U2E/domain/news/service/NewsInfoServiceTest.java new file mode 100644 index 0000000..bc401ac --- /dev/null +++ b/src/test/java/Konkuk/U2E/domain/news/service/NewsInfoServiceTest.java @@ -0,0 +1,98 @@ +package Konkuk.U2E.domain.news.service; + +import Konkuk.U2E.domain.news.domain.News; +import Konkuk.U2E.domain.news.dto.response.GetNewsInfoResponse; +import Konkuk.U2E.domain.news.exception.NewsNotFoundException; +import Konkuk.U2E.domain.news.repository.NewsRepository; +import Konkuk.U2E.domain.news.service.mapper.NewsMapperFactory; +import Konkuk.U2E.domain.news.service.mapper.NewsMappingResult; +import Konkuk.U2E.global.openApi.gemini.service.NewsAiService; +import Konkuk.U2E.global.openApi.gemini.service.NewsRegionUpsertService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.util.StringUtils; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +class NewsInfoServiceTest { + + NewsRepository newsRepository = Mockito.mock(NewsRepository.class); + NewsMapperFactory newsMapperFactory = Mockito.mock(NewsMapperFactory.class); + NewsAiService newsAiService = Mockito.mock(NewsAiService.class); + NewsRegionUpsertService newsRegionUpsertService = Mockito.mock(NewsRegionUpsertService.class); + + NewsInfoService service = new NewsInfoService( + newsRepository, + newsMapperFactory, + newsAiService, + newsRegionUpsertService + ); + + @Test + @DisplayName("newsId로 조회 성공 시, 엔티티와 매핑 결과가 GetNewsInfoResponse 로 반환된다") + void getNewsInfo_success() { + // given: 엔티티 더미 + News news = News.builder() + .newsUrl("https://example.com/news") + .imageUrl("https://example.com/img.jpg") + .newsTitle("제목") + .newsBody("본문 원문(길게)") + .newsDate(LocalDate.of(2025,10,29)) + .climateList(List.of()) + .build(); + + // AI 필드 세팅 + news.applyAiResult( + "AI 솔루션", + "관련1", "https://a.com", + "관련2", "https://b.com", + "관련3", "https://c.com" + ); + news.applyAiSummary("요약 본문"); + + when(newsRepository.findById(anyLong())).thenReturn(Optional.of(news)); + + // 매퍼 스텁: climate/region 을 세팅한 NewsMappingResult 반환 + when(newsMapperFactory.newsMappingFunction()) + .thenReturn((News n) -> new NewsMappingResult( + List.of(), // climateProblems + List.of("Seoul","Busan"), // regionNames + n + )); + + // when + GetNewsInfoResponse resp = service.getNewsInfo(1L); + + // then + assertThat(resp).isNotNull(); + assertThat(resp.newsTitle()).isEqualTo("제목"); + assertThat(resp.newsUrl()).isEqualTo("https://example.com/news"); + assertThat(resp.newsImageUrl()).isEqualTo("https://example.com/img.jpg"); + // 주의: newsBody 는 entity.aiSummary 로 매핑됨 + assertThat(resp.newsBody()).isEqualTo("요약 본문"); + assertThat(resp.newsDate()).isEqualTo("2025-10-29"); + assertThat(resp.regionList()).containsExactly("Seoul","Busan"); + assertThat(resp.aiSolution()).isEqualTo("AI 솔루션"); + assertThat(resp.aiRelated()).hasSize(3); + assertThat(StringUtils.hasText(resp.aiRelated().get(0).title())).isTrue(); + assertThat(StringUtils.hasText(resp.aiRelated().get(0).url())).isTrue(); + } + + @Test + @DisplayName("newsId 조회 실패 시 NewsNotFoundException 발생") + void getNewsInfo_notFound() { + // given + when(newsRepository.findById(anyLong())).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> service.getNewsInfo(999L)) + .isInstanceOf(NewsNotFoundException.class); + } +} \ No newline at end of file diff --git a/src/test/java/Konkuk/U2E/domain/news/service/NewsLatelyServiceTest.java b/src/test/java/Konkuk/U2E/domain/news/service/NewsLatelyServiceTest.java new file mode 100644 index 0000000..8b09898 --- /dev/null +++ b/src/test/java/Konkuk/U2E/domain/news/service/NewsLatelyServiceTest.java @@ -0,0 +1,58 @@ +package Konkuk.U2E.domain.news.service; + +import Konkuk.U2E.domain.news.domain.News; +import Konkuk.U2E.domain.news.dto.response.GetLatelyNewsResponse; +import Konkuk.U2E.domain.news.repository.NewsRepository; +import Konkuk.U2E.domain.news.service.mapper.NewsMapperFactory; +import Konkuk.U2E.domain.news.service.mapper.NewsMappingResult; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +class NewsLatelyServiceTest { + + NewsRepository newsRepository = Mockito.mock(NewsRepository.class); + NewsMapperFactory newsMapperFactory = Mockito.mock(NewsMapperFactory.class); + NewsLatelyService service = new NewsLatelyService(newsRepository, newsMapperFactory); + + @Test + @DisplayName("최신 5건 조회 후 매핑하여 GetLatelyNewsResponse 로 반환한다") + void getLatelyNews_success() { + // given: 더미 엔티티 + News n1 = News.builder() + .newsUrl("u1").imageUrl("i1").newsTitle("t1") + .newsBody("b1").newsDate(LocalDate.of(2025,10,28)) + .climateList(List.of()) + .build(); + + News n2 = News.builder() + .newsUrl("u2").imageUrl("i2").newsTitle("t2") + .newsBody("b2").newsDate(LocalDate.of(2025,10,27)) + .climateList(List.of()) + .build(); + + // repository 스텁 + when(newsRepository.findTop5ByOrderByNewsDateDesc()).thenReturn(List.of(n1, n2)); + + // mapper 스텁: factory.newsMappingFunction() 호출 시 Function 반환 + when(newsMapperFactory.newsMappingFunction()).thenReturn(news -> { + // 예시: regionNames 는 고정, climateProblems 비워둠 + return new NewsMappingResult(List.of(), List.of("Seoul"), news); + }); + + // when + GetLatelyNewsResponse resp = service.getLatelyNews(); + + // then + assertThat(resp).isNotNull(); + assertThat(resp.latelyNewsList()).hasSize(2); + assertThat(resp.latelyNewsList().get(0).newsTitle()).isEqualTo("t1"); + assertThat(resp.latelyNewsList().get(0).regionList()).containsExactly("Seoul"); + } +} \ No newline at end of file From 5e421c0e0968e1d920135a0e6a38d70ae298d451 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Wed, 29 Oct 2025 16:22:32 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 20 ++++++++++++-------- src/test/resources/application-test.yml | 4 ++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 6255c92..c2c626b 100644 --- a/build.gradle +++ b/build.gradle @@ -61,14 +61,18 @@ dependencies { testImplementation 'org.mockito:mockito-core:5.12.0' testImplementation 'org.assertj:assertj-core:3.26.0' - // WebClient - implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'com.fasterxml.jackson.core:jackson-databind' - implementation 'com.fasterxml.jackson.core:jackson-annotations' - implementation 'com.fasterxml.jackson.core:jackson-core' - - // 헬스 체크 api 를 사용하기 위한 Actuator 의존성 추가 - implementation 'org.springframework.boot:spring-boot-starter-actuator' + // WebClient + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'com.fasterxml.jackson.core:jackson-annotations' + implementation 'com.fasterxml.jackson.core:jackson-core' + + // 헬스 체크 api 를 사용하기 위한 Actuator 의존성 추가 + implementation 'org.springframework.boot:spring-boot-starter-actuator' +} + +test { + useJUnitPlatform() } jacoco { diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index dfb0959..b0d665c 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -28,3 +28,7 @@ spring: dialect: org.hibernate.dialect.H2Dialect +gemini: + api-key: abcdefghijklmnopqrstuvwxyz + model: gemini-2.5-flash + endpoint: https://generativelanguage.googleapis.com/v1beta \ No newline at end of file From c47f0b7a2eb55cd7acacbaf9734ef740528838a3 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Wed, 29 Oct 2025 17:02:48 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[test]=20Pin=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=ED=86=B5=ED=95=A9/=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pin/controller/PinControllerTest.java | 134 ++++++++++++++ .../pin/service/PinInfoServiceTest.java | 165 ++++++++++++++++++ .../pin/service/PinListServiceTest.java | 60 +++++++ 3 files changed, 359 insertions(+) create mode 100644 src/test/java/Konkuk/U2E/domain/pin/controller/PinControllerTest.java create mode 100644 src/test/java/Konkuk/U2E/domain/pin/service/PinInfoServiceTest.java create mode 100644 src/test/java/Konkuk/U2E/domain/pin/service/PinListServiceTest.java diff --git a/src/test/java/Konkuk/U2E/domain/pin/controller/PinControllerTest.java b/src/test/java/Konkuk/U2E/domain/pin/controller/PinControllerTest.java new file mode 100644 index 0000000..18f9e59 --- /dev/null +++ b/src/test/java/Konkuk/U2E/domain/pin/controller/PinControllerTest.java @@ -0,0 +1,134 @@ +package Konkuk.U2E.domain.pin.controller; + +import Konkuk.U2E.domain.pin.dto.response.GetPinInfoResponse; +import Konkuk.U2E.domain.pin.dto.response.GetPinListResponse; +import Konkuk.U2E.domain.pin.dto.response.NewsInfo; +import Konkuk.U2E.domain.pin.dto.response.PinInfo; +import Konkuk.U2E.domain.news.domain.ClimateProblem; +import Konkuk.U2E.domain.pin.service.PinInfoService; +import Konkuk.U2E.domain.pin.service.PinListService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.math.BigDecimal; +import java.util.List; + +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +class PinControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockitoBean + PinInfoService pinInfoService; + + @MockitoBean + PinListService pinListService; + + @Nested + @DisplayName("GET /pin/{pinId}") + class PinInfoApi { + + @Test + @DisplayName("핀 상세 – BaseResponse(data.newsList) 반환") + void viewPinInfo() throws Exception { + GetPinInfoResponse stub = GetPinInfoResponse.of( + List.of( + new NewsInfo(List.of(ClimateProblem.TEMPERATURE_RISE), 1L, "뉴스1", "본문1", "2025-10-29"), + new NewsInfo(List.of(ClimateProblem.HEAVY_RAIN_OR_FLOOD), 2L, "뉴스2", "본문2", "2025-10-28") + ) + ); + + Mockito.when(pinInfoService.getPinInfo(anyLong())).thenReturn(stub); + + mockMvc.perform(get("/pin/{pinId}", 100L).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.data.newsList", hasSize(2))) + .andExpect(jsonPath("$.data.newsList[0].newsId").value(1L)) + .andExpect(jsonPath("$.data.newsList[0].climateList[0]").value("TEMPERATURE_RISE")) + .andExpect(jsonPath("$.data.newsList[1].newsTitle").value("뉴스2")); + } + } + + @Nested + @DisplayName("GET /pin (쿼리 파라미터 조합)") + class PinListApi { + + @Test + @DisplayName("전체 핀 목록 – 파라미터 없음") + void viewPinList_all() throws Exception { + GetPinListResponse stub = GetPinListResponse.of( + List.of( + new PinInfo(1L, new BigDecimal("37.5665"), new BigDecimal("126.9780"), true, "Seoul", List.of()), + new PinInfo(2L, new BigDecimal("35.1796"), new BigDecimal("129.0756"), false, "Busan", List.of(ClimateProblem.FINE_DUST)) + ) + ); + + Mockito.when(pinListService.getPinList(isNull(), isNull(), isNull())).thenReturn(stub); + + mockMvc.perform(get("/pin").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.pinList", hasSize(2))) + .andExpect(jsonPath("$.data.pinList[0].pinId").value(1L)) + .andExpect(jsonPath("$.data.pinList[1].region").value("Busan")); + } + + @Test + @DisplayName("지역으로 필터 – region=Seoul") + void viewPinList_region() throws Exception { + GetPinListResponse stub = GetPinListResponse.of( + List.of(new PinInfo(3L, new BigDecimal("37.5"), new BigDecimal("127.0"), true, "Seoul", List.of())) + ); + Mockito.when(pinListService.getPinList(eq("Seoul"), isNull(), isNull())).thenReturn(stub); + + mockMvc.perform(get("/pin").param("region", "Seoul").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.pinList", hasSize(1))) + .andExpect(jsonPath("$.data.pinList[0].region").value("Seoul")); + } + + @Test + @DisplayName("기후로 필터 – climate=TEMPERATURE_RISE") + void viewPinList_climate() throws Exception { + GetPinListResponse stub = GetPinListResponse.of( + List.of(new PinInfo(4L, new BigDecimal("33.0"), new BigDecimal("129.0"), false, "Jeju", List.of(ClimateProblem.TEMPERATURE_RISE))) + ); + Mockito.when(pinListService.getPinList(isNull(), eq("TEMPERATURE_RISE"), isNull())).thenReturn(stub); + + mockMvc.perform(get("/pin").param("climate", "TEMPERATURE_RISE").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.pinList[0].climateProblem[0]").value("TEMPERATURE_RISE")); + } + + @Test + @DisplayName("뉴스 카드 핀 목록 – newsId=10") + void viewPinList_newsId() throws Exception { + GetPinListResponse stub = GetPinListResponse.of( + List.of(new PinInfo(5L, new BigDecimal("36.0"), new BigDecimal("128.0"), true, "Daegu", List.of())) + ); + Mockito.when(pinListService.getPinList(isNull(), isNull(), eq(10L))).thenReturn(stub); + + mockMvc.perform(get("/pin").param("newsId", "10").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.pinList[0].pinId").value(5L)) + .andExpect(jsonPath("$.data.pinList[0].region").value("Daegu")); + } + } +} \ No newline at end of file diff --git a/src/test/java/Konkuk/U2E/domain/pin/service/PinInfoServiceTest.java b/src/test/java/Konkuk/U2E/domain/pin/service/PinInfoServiceTest.java new file mode 100644 index 0000000..dc24cbd --- /dev/null +++ b/src/test/java/Konkuk/U2E/domain/pin/service/PinInfoServiceTest.java @@ -0,0 +1,165 @@ +package Konkuk.U2E.domain.pin.service; + +import Konkuk.U2E.domain.news.domain.Climate; +import Konkuk.U2E.domain.news.domain.ClimateProblem; +import Konkuk.U2E.domain.news.domain.News; +import Konkuk.U2E.domain.news.repository.ClimateRepository; +import Konkuk.U2E.domain.news.repository.NewsPinRepository; +import Konkuk.U2E.domain.pin.domain.Pin; +import Konkuk.U2E.domain.pin.domain.Region; +import Konkuk.U2E.domain.pin.dto.response.GetPinListResponse; +import Konkuk.U2E.domain.pin.dto.response.PinInfo; +import Konkuk.U2E.domain.pin.exception.InvalidParamException; +import Konkuk.U2E.domain.pin.exception.NewsPinNotFoundException; +import Konkuk.U2E.domain.pin.exception.PinNewsNotFoundException; +import Konkuk.U2E.domain.pin.repository.PinRepository; +import Konkuk.U2E.domain.pin.repository.RegionRepository; +import Konkuk.U2E.global.util.DateUtil; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@ActiveProfiles("test") +@SpringBootTest(classes = PinListService.class) +class PinListServiceTest { + + @MockitoBean ClimateRepository climateRepository; + @MockitoBean NewsPinRepository newsPinRepository; + @MockitoBean PinRepository pinRepository; + @MockitoBean RegionRepository regionRepository; + @MockitoBean DateUtil dateUtil; + + @Resource + PinListService service; + + Region seoul; + Region busan; + Pin pin1; + Pin pin2; + + @BeforeEach + void setup() { + seoul = Region.builder() + .name("Seoul") + .latitude(new BigDecimal("37.5665")) + .longitude(new BigDecimal("126.9780")) + .build(); + busan = Region.builder() + .name("Busan") + .latitude(new BigDecimal("35.1796")) + .longitude(new BigDecimal("129.0756")) + .build(); + + // 테스트에서는 pinId(null) 그대로 사용 + pin1 = Pin.builder().region(seoul).build(); + pin2 = Pin.builder().region(busan).build(); + } + + @Test + @DisplayName("전체 핀 – 모든 파라미터 null") + void getPinList_all() { + when(pinRepository.findAll()).thenReturn(List.of(pin1, pin2)); + + News news1 = News.builder().newsUrl("u1").imageUrl("i1").newsTitle("t1") + .newsBody("b1").newsDate(LocalDate.of(2025,10,20)).climateList(List.of()).build(); + News news2 = News.builder().newsUrl("u2").imageUrl("i2").newsTitle("t2") + .newsBody("b2").newsDate(LocalDate.of(2025,10,18)).climateList(List.of()).build(); + + // 중요: pinId가 null이므로 anyLong()이 아닌 any() 사용 (null 매칭) + when(newsPinRepository.findNewsByPinId(any())).thenReturn(List.of(news1), List.of(news2)); + + when(dateUtil.getLatestNews(anyList())).thenAnswer(inv -> ((List) inv.getArgument(0)).get(0)); + when(dateUtil.checkLately(any())).thenReturn(true); + when(climateRepository.findClimatesByNews(any())).thenReturn(List.of()); + + GetPinListResponse resp = service.getPinList(null, null, null); + + assertThat(resp.pinList()).hasSize(2); + assertThat(resp.pinList().get(0).region()).isEqualTo("Seoul"); + assertThat(resp.pinList().get(0).isLately()).isTrue(); + } + + @Test + @DisplayName("지역 필터 – region=Seoul") + void getPinList_region() { + when(regionRepository.findRegionsByName(eq("Seoul"))).thenReturn(List.of(seoul)); + when(pinRepository.findPinByRegion(seoul)).thenReturn(pin1); + + News news = News.builder().newsUrl("u").imageUrl("i").newsTitle("t") + .newsBody("b").newsDate(LocalDate.of(2025,10,10)).climateList(List.of()).build(); + + when(newsPinRepository.findNewsByPinId(any())).thenReturn(List.of(news)); + when(dateUtil.getLatestNews(anyList())).thenReturn(news); + when(dateUtil.checkLately(any())).thenReturn(false); + when(climateRepository.findClimatesByNews(any())).thenReturn(List.of()); + + GetPinListResponse resp = service.getPinList("Seoul", null, null); + + assertThat(resp.pinList()).hasSize(1); + PinInfo info = resp.pinList().get(0); + assertThat(info.region()).isEqualTo("Seoul"); + assertThat(info.isLately()).isFalse(); + } + + @Test + @DisplayName("뉴스ID 필터 – newsId=10 (핀 없으면 예외)") + void getPinList_newsId_empty() { + when(newsPinRepository.findPinsByNewsId(eq(10L))).thenReturn(List.of()); + assertThatThrownBy(() -> service.getPinList(null, null, 10L)) + .isInstanceOf(NewsPinNotFoundException.class); + } + + @Test + @DisplayName("뉴스ID 필터 – 정상 반환") + void getPinList_newsId_ok() { + when(newsPinRepository.findPinsByNewsId(eq(10L))).thenReturn(List.of(pin1)); + + News n = News.builder().newsUrl("u").imageUrl("i").newsTitle("t") + .newsBody("b").newsDate(LocalDate.of(2025,10,1)).climateList(List.of()).build(); + + // 중요: pinId가 null이라 any() 사용 + when(newsPinRepository.findNewsByPinId(any())).thenReturn(List.of(n)); + when(dateUtil.getLatestNews(anyList())).thenReturn(n); + when(dateUtil.checkLately(any())).thenReturn(true); + when(climateRepository.findClimatesByNews(any())).thenReturn(List.of()); + + GetPinListResponse resp = service.getPinList(null, null, 10L); + + assertThat(resp.pinList()).hasSize(1); + assertThat(resp.pinList().get(0).region()).isEqualTo("Seoul"); + } + + @Test + @DisplayName("잘못된 파라미터 조합 – InvalidParamException") + void getPinList_invalidParams() { + assertThatThrownBy(() -> service.getPinList("Seoul", "TEMPERATURE_RISE", null)) + .isInstanceOf(InvalidParamException.class); + assertThatThrownBy(() -> service.getPinList("Seoul", null, 1L)) + .isInstanceOf(InvalidParamException.class); + assertThatThrownBy(() -> service.getPinList(null, "TEMPERATURE_RISE", 1L)) + .isInstanceOf(InvalidParamException.class); + } + + @Test + @DisplayName("뉴스 없는 핀 – createPinInfo 내 예외 흐름") + void createPinInfo_noNews() { + when(pinRepository.findAll()).thenReturn(List.of(pin1)); + // 중요: any()로 null 매칭 + when(newsPinRepository.findNewsByPinId(any())).thenReturn(List.of()); + + assertThatThrownBy(() -> service.getPinList(null, null, null)) + .isInstanceOf(PinNewsNotFoundException.class); + } +} \ No newline at end of file diff --git a/src/test/java/Konkuk/U2E/domain/pin/service/PinListServiceTest.java b/src/test/java/Konkuk/U2E/domain/pin/service/PinListServiceTest.java new file mode 100644 index 0000000..d0c1af5 --- /dev/null +++ b/src/test/java/Konkuk/U2E/domain/pin/service/PinListServiceTest.java @@ -0,0 +1,60 @@ +package Konkuk.U2E.domain.pin.service; + +import Konkuk.U2E.domain.news.domain.News; +import Konkuk.U2E.domain.news.repository.ClimateRepository; +import Konkuk.U2E.domain.news.repository.NewsPinRepository; +import Konkuk.U2E.domain.pin.dto.response.GetPinInfoResponse; +import Konkuk.U2E.domain.pin.dto.response.NewsInfo; +import Konkuk.U2E.domain.pin.exception.PinNewsNotFoundException; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +@ActiveProfiles("test") +@SpringBootTest(classes = PinInfoService.class) +class PinInfoServiceTest { + + @MockitoBean NewsPinRepository newsPinRepository; + @MockitoBean ClimateRepository climateRepository; + + @Resource + PinInfoService service; + + @Test + @DisplayName("핀에 연결된 뉴스가 있으면 DTO로 매핑하여 반환") + void getPinInfo_success() { + News n1 = News.builder().newsUrl("u1").imageUrl("i1").newsTitle("t1") + .newsBody("b1").newsDate(LocalDate.of(2025,10,28)).climateList(List.of()).build(); + News n2 = News.builder().newsUrl("u2").imageUrl("i2").newsTitle("t2") + .newsBody("b2").newsDate(LocalDate.of(2025,10,27)).climateList(List.of()).build(); + + when(newsPinRepository.findNewsByPinId(anyLong())).thenReturn(List.of(n1, n2)); + when(climateRepository.findClimatesByNews(any())).thenReturn(List.of()); + + GetPinInfoResponse resp = service.getPinInfo(99L); + + assertThat(resp.newsList()).hasSize(2); + NewsInfo first = resp.newsList().get(0); + assertThat(first.newsTitle()).isEqualTo("t1"); + assertThat(first.newsDate()).isEqualTo("2025-10-28"); + } + + @Test + @DisplayName("뉴스 없으면 PinNewsNotFoundException") + void getPinInfo_empty() { + when(newsPinRepository.findNewsByPinId(anyLong())).thenReturn(List.of()); + assertThatThrownBy(() -> service.getPinInfo(1L)) + .isInstanceOf(PinNewsNotFoundException.class); + } +} \ No newline at end of file From 76e908f10c27f8399650f4f67de68385ec4fdc5e Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Wed, 29 Oct 2025 17:30:26 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[chore]=20gradle=20task=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 119 ++++++++++++++++++++++----------------------------- 1 file changed, 51 insertions(+), 68 deletions(-) diff --git a/build.gradle b/build.gradle index c2c626b..5499076 100644 --- a/build.gradle +++ b/build.gradle @@ -72,95 +72,78 @@ dependencies { } test { + jacoco { + destinationFile = file("$buildDir/jacoco/test.exec") + } + useJUnitPlatform() + + finalizedBy 'jacocoTestReport' } jacoco { toolVersion = "0.8.12" } -tasks.named('test') { - useJUnitPlatform() - finalizedBy tasks.jacocoTestReport -} - -tasks.named('jacocoTestReport', JacocoReport) { - dependsOn(tasks.test) - - def mainClasses = files(sourceSets.main.output).asFileTree.matching { - exclude( - "**/generated/**", - "**/build/**", - "**/*application*", - "**/*config*", - "**/*dto*", - "**/*request*", - "**/*response*", - "**/generated/querydsl/**", - "**/Q*.*" - ) - } - - additionalSourceDirs.from files(sourceSets.main.allSource.srcDirs) - sourceDirectories.from files(sourceSets.main.allSource.srcDirs) - classDirectories.setFrom(mainClasses) - - executionData.from fileTree(project.rootDir) { - include "**/build/jacoco/*.exec" - } - +jacocoTestReport { + dependsOn test reports { - html.required.set(true) - html.outputLocation.set(layout.buildDirectory.dir("reports/test/jacocoTestReportHtml")) - - xml.required.set(true) - xml.outputLocation.set(layout.buildDirectory.file("reports/test/jacocoTestReport.xml")) - + html.required.set(true) // 개발자용 HTML 보고서 생성 + xml.required.set(true) // SonarCloud용 XML 보고서 생성 csv.required.set(false) } + afterEvaluate { + classDirectories.setFrom( + files(classDirectories.files.collect { + fileTree(dir: it, excludes: [ + "**/generated/**", + "**/build/**", + "**/*application*", + "**/*config*", + "**/*dto*", + "**/*request*", + "**/*response*", + "**/generated/querydsl/**", + "**/Q*.*" + ]) + }) + ) + } + + finalizedBy 'jacocoTestCoverageVerification' } -tasks.named('jacocoTestCoverageVerification', JacocoCoverageVerification) { - dependsOn(tasks.test) - - classDirectories.setFrom( - files(sourceSets.main.output).asFileTree.matching { - exclude( - "**/generated/**", - "**/build/**", - "**/*application*", - "**/*config*", - "**/*dto*", - "**/*request*", - "**/*response*", - "**/generated/querydsl/**", - "**/Q*.*" - ) - } - ) +jacocoTestCoverageVerification { violationRules { rule { - element = 'CLASS' - limit { - counter = 'INSTRUCTION' - value = 'COVEREDRATIO' - minimum = 0.00 - } - limit { - counter = 'BRANCH' - value = 'COVEREDRATIO' - minimum = 0.00 - } + enabled = true // 활성화 limit { - counter = 'LINE' - value = 'COVEREDRATIO' - minimum = 0.00 + element = 'CLASS' + minimum = 0.0 } + + excludes = [ + "**/generated/**", + "**/build/**", + "**/*application*", + "**/*config*", + "**/*dto*", + "**/*request*", + "**/*response*", + "**/generated/querydsl/**", + "**/Q*.*" + ] } } } +tasks.register('testCoverage') { + group = 'verification' + description = 'Runs the unit tests with coverage' + dependsOn('test', 'jacocoTestReport', 'jacocoTestCoverageVerification') +} + tasks.named('check') { dependsOn tasks.named('jacocoTestCoverageVerification') } From dafdb52cb3a28e36da748928da209a47a424c267 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 30 Oct 2025 01:03:55 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BB=A4=EB=B2=84=EB=A6=AC=EC=A7=80=20=EB=86=92=EC=9D=B4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 133 ++++++----- .../domain/pin/service/PinListService.java | 10 +- .../news/service/NewsInfoServiceTest.java | 37 ++- .../service/mapper/NewsMapperFactoryTest.java | 92 ++++++++ .../pin/service/PinInfoServiceTest.java | 155 ++----------- .../pin/service/PinListServiceTest.java | 216 +++++++++++++++--- 6 files changed, 426 insertions(+), 217 deletions(-) create mode 100644 src/test/java/Konkuk/U2E/domain/news/service/mapper/NewsMapperFactoryTest.java diff --git a/build.gradle b/build.gradle index 5499076..2ded8a7 100644 --- a/build.gradle +++ b/build.gradle @@ -52,9 +52,9 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // JWT - implementation 'io.jsonwebtoken:jjwt-api:0.11.5' - implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' - implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'io.jsonwebtoken:jjwt-api:0.11.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.3' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testImplementation 'io.rest-assured:spring-mock-mvc:5.4.0' @@ -72,82 +72,105 @@ dependencies { } test { - jacoco { - destinationFile = file("$buildDir/jacoco/test.exec") - } - useJUnitPlatform() - - finalizedBy 'jacocoTestReport' } jacoco { toolVersion = "0.8.12" } -jacocoTestReport { - dependsOn test - reports { - html.required.set(true) // 개발자용 HTML 보고서 생성 - xml.required.set(true) // SonarCloud용 XML 보고서 생성 - csv.required.set(false) - } - afterEvaluate { - classDirectories.setFrom( - files(classDirectories.files.collect { - fileTree(dir: it, excludes: [ - "**/generated/**", - "**/build/**", - "**/*application*", - "**/*config*", - "**/*dto*", - "**/*request*", - "**/*response*", - "**/generated/querydsl/**", - "**/Q*.*" - ]) - }) +tasks.named('test') { + useJUnitPlatform() + finalizedBy tasks.jacocoTestReport +} + +tasks.named('jacocoTestReport', JacocoReport) { + dependsOn(tasks.test) + + def mainClasses = files(sourceSets.main.output).asFileTree.matching { + exclude( + "**/generated/**", + "**/build/**", + "**/*application*", + "**/*config*", + "**/*dto*", + "**/*request*", + "**/*response*", + "**/generated/querydsl/**", + "**/exception/**", + "**/test/**", + "**/resolver/**", + "**/Q*.*" ) } - finalizedBy 'jacocoTestCoverageVerification' + additionalSourceDirs.from files(sourceSets.main.allSource.srcDirs) + sourceDirectories.from files(sourceSets.main.allSource.srcDirs) + classDirectories.setFrom(mainClasses) + + executionData.from fileTree(project.rootDir) { + include "**/build/jacoco/*.exec" + } + + reports { + html.required.set(true) + html.outputLocation.set(layout.buildDirectory.dir("reports/test/jacocoTestReportHtml")) + + xml.required.set(true) + xml.outputLocation.set(layout.buildDirectory.file("reports/test/jacocoTestReport.xml")) + + csv.required.set(false) + } } -jacocoTestCoverageVerification { +tasks.named('jacocoTestCoverageVerification', JacocoCoverageVerification) { + dependsOn(tasks.test) + + classDirectories.setFrom( + files(sourceSets.main.output).asFileTree.matching { + exclude( + "**/generated/**", + "**/build/**", + "**/*application*", + "**/*config*", + "**/*dto*", + "**/*request*", + "**/*response*", + "**/generated/querydsl/**", + "**/exception/**", + "**/test/**", + "**/resolver/**", + "**/Q*.*" + ) + } + ) violationRules { rule { - enabled = true // 활성화 + element = 'CLASS' limit { - element = 'CLASS' - minimum = 0.0 + counter = 'INSTRUCTION' + value = 'COVEREDRATIO' + minimum = 0.00 + } + limit { + counter = 'BRANCH' + value = 'COVEREDRATIO' + minimum = 0.00 + } + limit { + counter = 'LINE' + value = 'COVEREDRATIO' + minimum = 0.00 } - - excludes = [ - "**/generated/**", - "**/build/**", - "**/*application*", - "**/*config*", - "**/*dto*", - "**/*request*", - "**/*response*", - "**/generated/querydsl/**", - "**/Q*.*" - ] } } } -tasks.register('testCoverage') { - group = 'verification' - description = 'Runs the unit tests with coverage' - dependsOn('test', 'jacocoTestReport', 'jacocoTestCoverageVerification') -} - tasks.named('check') { dependsOn tasks.named('jacocoTestCoverageVerification') } tasks.named('sonarqube') { dependsOn tasks.jacocoTestReport -} +} \ No newline at end of file diff --git a/src/main/java/Konkuk/U2E/domain/pin/service/PinListService.java b/src/main/java/Konkuk/U2E/domain/pin/service/PinListService.java index 33407ba..a1cdbef 100644 --- a/src/main/java/Konkuk/U2E/domain/pin/service/PinListService.java +++ b/src/main/java/Konkuk/U2E/domain/pin/service/PinListService.java @@ -109,15 +109,15 @@ private PinInfo createPinInfo(Pin pin) { } private PinInfo createPinInfo(Pin pin, ClimateProblem climateProblem) { - List newsList = newsPinRepository.findNewsByPinId(pin.getPinId()); + List newsList = newsPinRepository.findNewsByPinId(pin.getPinId()).stream() + .filter(news -> climateRepository.existsByNewsAndClimateProblem(news, climateProblem)) //사용자가 필터링을 요청한 ClimateProblem을 포함하지 않는 경우 제거 + .toList(); + if (newsList.isEmpty()) { // 뉴스가 없는 핀은 존재할 수 없으므로 예외처리 throw new PinNewsNotFoundException(PINNEWS_NOT_FOUND); } - News latestNews = dateUtil.getLatestNews( - newsList.stream() - .filter(news -> climateRepository.existsByNewsAndClimateProblem(news, climateProblem)).toList() //사용자가 필터링을 요청한 ClimateProblem을 포함하지 않는 경우 제거 - ); + News latestNews = dateUtil.getLatestNews(newsList); boolean isLately = dateUtil.checkLately(latestNews.getNewsDate()); List climateProblems = climateRepository.findClimatesByNews(latestNews).stream() diff --git a/src/test/java/Konkuk/U2E/domain/news/service/NewsInfoServiceTest.java b/src/test/java/Konkuk/U2E/domain/news/service/NewsInfoServiceTest.java index bc401ac..b439079 100644 --- a/src/test/java/Konkuk/U2E/domain/news/service/NewsInfoServiceTest.java +++ b/src/test/java/Konkuk/U2E/domain/news/service/NewsInfoServiceTest.java @@ -48,13 +48,14 @@ void getNewsInfo_success() { .climateList(List.of()) .build(); - // AI 필드 세팅 + // AI 필드 세팅 (3개의 관련 기사 모두 존재하도록) news.applyAiResult( "AI 솔루션", "관련1", "https://a.com", "관련2", "https://b.com", "관련3", "https://c.com" ); + // 요약(= dto.newsBody에 매핑됨) news.applyAiSummary("요약 본문"); when(newsRepository.findById(anyLong())).thenReturn(Optional.of(news)); @@ -95,4 +96,38 @@ void getNewsInfo_notFound() { assertThatThrownBy(() -> service.getNewsInfo(999L)) .isInstanceOf(NewsNotFoundException.class); } + + @Test + @DisplayName("AI 관련 필드들이 비어있으면 aiRelated는 빈 리스트이며 aiSolution/newsBody도 null을 그대로 반환한다") + void getNewsInfo_noRelatedOrSummary() { + // given: 관련 기사/솔루션/요약이 모두 비어있는 엔티티 + News news = News.builder() + .newsUrl("https://example.com/news") + .imageUrl("https://example.com/img.jpg") + .newsTitle("제목 없음 가능") + .newsBody("원문 본문") + .newsDate(LocalDate.of(2025,10,30)) + .climateList(List.of()) + .build(); + + // aiResult/aiSummary 미설정 → 모두 null + when(newsRepository.findById(anyLong())).thenReturn(Optional.of(news)); + + // 매퍼 스텁: 정상 region/climate 세팅 + when(newsMapperFactory.newsMappingFunction()) + .thenReturn((News n) -> new NewsMappingResult( + List.of(), List.of("Seoul"), n + )); + + // when + GetNewsInfoResponse resp = service.getNewsInfo(2L); + + // then + assertThat(resp).isNotNull(); + // aiSummary가 null이면 dto.newsBody도 null (현재 매핑 로직 기준) + assertThat(resp.newsBody()).isNull(); + assertThat(resp.aiSolution()).isNull(); + assertThat(resp.aiRelated()).isEmpty(); // fromEntity가 아무것도 추가하지 않음 + assertThat(resp.regionList()).containsExactly("Seoul"); + } } \ No newline at end of file diff --git a/src/test/java/Konkuk/U2E/domain/news/service/mapper/NewsMapperFactoryTest.java b/src/test/java/Konkuk/U2E/domain/news/service/mapper/NewsMapperFactoryTest.java new file mode 100644 index 0000000..80b997c --- /dev/null +++ b/src/test/java/Konkuk/U2E/domain/news/service/mapper/NewsMapperFactoryTest.java @@ -0,0 +1,92 @@ +package Konkuk.U2E.domain.news.service.mapper; + +import Konkuk.U2E.domain.news.domain.Climate; +import Konkuk.U2E.domain.news.domain.ClimateProblem; +import Konkuk.U2E.domain.news.domain.News; +import Konkuk.U2E.domain.news.repository.ClimateRepository; +import Konkuk.U2E.domain.news.repository.NewsPinRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.time.LocalDate; +import java.util.List; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +class NewsMapperFactoryTest { + + ClimateRepository climateRepository = Mockito.mock(ClimateRepository.class); + NewsPinRepository newsPinRepository = Mockito.mock(NewsPinRepository.class); + + NewsMapperFactory factory = new NewsMapperFactory(climateRepository, newsPinRepository); + + @Test + @DisplayName("news → (climateProblems, regionNames, news) 매핑 정상 동작") + void mapping_success() { + // given + News news = News.builder() + .newsUrl("https://example.com/news") + .imageUrl("https://example.com/img.jpg") + .newsTitle("제목") + .newsBody("본문") + .newsDate(LocalDate.of(2025, 10, 30)) + .climateList(List.of()) // 엔티티 내부용 리스트(무관) + .build(); + + // region 이름 조회는 newsId(null) 일 수 있으므로 anyLong() 대신 any() 사용 + when(newsPinRepository.findRegionNameByNews(any())) + .thenReturn(List.of("Seoul", "Busan")); + + // 기후 문제 리스트는 Climate -> ClimateProblem 으로 매핑됨 + when(climateRepository.findClimatesByNews(any(News.class))) + .thenReturn(List.of( + Climate.builder().climateProblem(ClimateProblem.TEMPERATURE_RISE).news(news).build(), + Climate.builder().climateProblem(ClimateProblem.HEAVY_RAIN_OR_FLOOD).news(news).build() + )); + + Function fn = factory.newsMappingFunction(); + + // when + NewsMappingResult result = fn.apply(news); + + // then + assertThat(result).isNotNull(); + assertThat(result.news()).isSameAs(news); + + assertThat(result.regionNames()).containsExactly("Seoul", "Busan"); + + assertThat(result.climateProblems()) + .containsExactly( + ClimateProblem.TEMPERATURE_RISE, + ClimateProblem.HEAVY_RAIN_OR_FLOOD + ); + } + + @Test + @DisplayName("연관 데이터가 없으면 빈 리스트로 매핑된다") + void mapping_emptyLists() { + // given + News news = News.builder() + .newsUrl("u").imageUrl("i").newsTitle("t") + .newsBody("b").newsDate(LocalDate.of(2025, 10, 29)) + .climateList(List.of()) + .build(); + + when(newsPinRepository.findRegionNameByNews(any())).thenReturn(List.of()); + when(climateRepository.findClimatesByNews(any(News.class))).thenReturn(List.of()); + + Function fn = factory.newsMappingFunction(); + + // when + NewsMappingResult result = fn.apply(news); + + // then + assertThat(result.regionNames()).isEmpty(); + assertThat(result.climateProblems()).isEmpty(); + assertThat(result.news()).isSameAs(news); + } +} \ No newline at end of file diff --git a/src/test/java/Konkuk/U2E/domain/pin/service/PinInfoServiceTest.java b/src/test/java/Konkuk/U2E/domain/pin/service/PinInfoServiceTest.java index dc24cbd..d0c1af5 100644 --- a/src/test/java/Konkuk/U2E/domain/pin/service/PinInfoServiceTest.java +++ b/src/test/java/Konkuk/U2E/domain/pin/service/PinInfoServiceTest.java @@ -1,165 +1,60 @@ package Konkuk.U2E.domain.pin.service; -import Konkuk.U2E.domain.news.domain.Climate; -import Konkuk.U2E.domain.news.domain.ClimateProblem; import Konkuk.U2E.domain.news.domain.News; import Konkuk.U2E.domain.news.repository.ClimateRepository; import Konkuk.U2E.domain.news.repository.NewsPinRepository; -import Konkuk.U2E.domain.pin.domain.Pin; -import Konkuk.U2E.domain.pin.domain.Region; -import Konkuk.U2E.domain.pin.dto.response.GetPinListResponse; -import Konkuk.U2E.domain.pin.dto.response.PinInfo; -import Konkuk.U2E.domain.pin.exception.InvalidParamException; -import Konkuk.U2E.domain.pin.exception.NewsPinNotFoundException; +import Konkuk.U2E.domain.pin.dto.response.GetPinInfoResponse; +import Konkuk.U2E.domain.pin.dto.response.NewsInfo; import Konkuk.U2E.domain.pin.exception.PinNewsNotFoundException; -import Konkuk.U2E.domain.pin.repository.PinRepository; -import Konkuk.U2E.domain.pin.repository.RegionRepository; -import Konkuk.U2E.global.util.DateUtil; import jakarta.annotation.Resource; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.when; @ActiveProfiles("test") -@SpringBootTest(classes = PinListService.class) -class PinListServiceTest { +@SpringBootTest(classes = PinInfoService.class) +class PinInfoServiceTest { - @MockitoBean ClimateRepository climateRepository; @MockitoBean NewsPinRepository newsPinRepository; - @MockitoBean PinRepository pinRepository; - @MockitoBean RegionRepository regionRepository; - @MockitoBean DateUtil dateUtil; + @MockitoBean ClimateRepository climateRepository; @Resource - PinListService service; - - Region seoul; - Region busan; - Pin pin1; - Pin pin2; - - @BeforeEach - void setup() { - seoul = Region.builder() - .name("Seoul") - .latitude(new BigDecimal("37.5665")) - .longitude(new BigDecimal("126.9780")) - .build(); - busan = Region.builder() - .name("Busan") - .latitude(new BigDecimal("35.1796")) - .longitude(new BigDecimal("129.0756")) - .build(); - - // 테스트에서는 pinId(null) 그대로 사용 - pin1 = Pin.builder().region(seoul).build(); - pin2 = Pin.builder().region(busan).build(); - } - - @Test - @DisplayName("전체 핀 – 모든 파라미터 null") - void getPinList_all() { - when(pinRepository.findAll()).thenReturn(List.of(pin1, pin2)); - - News news1 = News.builder().newsUrl("u1").imageUrl("i1").newsTitle("t1") - .newsBody("b1").newsDate(LocalDate.of(2025,10,20)).climateList(List.of()).build(); - News news2 = News.builder().newsUrl("u2").imageUrl("i2").newsTitle("t2") - .newsBody("b2").newsDate(LocalDate.of(2025,10,18)).climateList(List.of()).build(); - - // 중요: pinId가 null이므로 anyLong()이 아닌 any() 사용 (null 매칭) - when(newsPinRepository.findNewsByPinId(any())).thenReturn(List.of(news1), List.of(news2)); - - when(dateUtil.getLatestNews(anyList())).thenAnswer(inv -> ((List) inv.getArgument(0)).get(0)); - when(dateUtil.checkLately(any())).thenReturn(true); - when(climateRepository.findClimatesByNews(any())).thenReturn(List.of()); - - GetPinListResponse resp = service.getPinList(null, null, null); - - assertThat(resp.pinList()).hasSize(2); - assertThat(resp.pinList().get(0).region()).isEqualTo("Seoul"); - assertThat(resp.pinList().get(0).isLately()).isTrue(); - } - - @Test - @DisplayName("지역 필터 – region=Seoul") - void getPinList_region() { - when(regionRepository.findRegionsByName(eq("Seoul"))).thenReturn(List.of(seoul)); - when(pinRepository.findPinByRegion(seoul)).thenReturn(pin1); - - News news = News.builder().newsUrl("u").imageUrl("i").newsTitle("t") - .newsBody("b").newsDate(LocalDate.of(2025,10,10)).climateList(List.of()).build(); - - when(newsPinRepository.findNewsByPinId(any())).thenReturn(List.of(news)); - when(dateUtil.getLatestNews(anyList())).thenReturn(news); - when(dateUtil.checkLately(any())).thenReturn(false); - when(climateRepository.findClimatesByNews(any())).thenReturn(List.of()); - - GetPinListResponse resp = service.getPinList("Seoul", null, null); - - assertThat(resp.pinList()).hasSize(1); - PinInfo info = resp.pinList().get(0); - assertThat(info.region()).isEqualTo("Seoul"); - assertThat(info.isLately()).isFalse(); - } - - @Test - @DisplayName("뉴스ID 필터 – newsId=10 (핀 없으면 예외)") - void getPinList_newsId_empty() { - when(newsPinRepository.findPinsByNewsId(eq(10L))).thenReturn(List.of()); - assertThatThrownBy(() -> service.getPinList(null, null, 10L)) - .isInstanceOf(NewsPinNotFoundException.class); - } + PinInfoService service; @Test - @DisplayName("뉴스ID 필터 – 정상 반환") - void getPinList_newsId_ok() { - when(newsPinRepository.findPinsByNewsId(eq(10L))).thenReturn(List.of(pin1)); - - News n = News.builder().newsUrl("u").imageUrl("i").newsTitle("t") - .newsBody("b").newsDate(LocalDate.of(2025,10,1)).climateList(List.of()).build(); - - // 중요: pinId가 null이라 any() 사용 - when(newsPinRepository.findNewsByPinId(any())).thenReturn(List.of(n)); - when(dateUtil.getLatestNews(anyList())).thenReturn(n); - when(dateUtil.checkLately(any())).thenReturn(true); + @DisplayName("핀에 연결된 뉴스가 있으면 DTO로 매핑하여 반환") + void getPinInfo_success() { + News n1 = News.builder().newsUrl("u1").imageUrl("i1").newsTitle("t1") + .newsBody("b1").newsDate(LocalDate.of(2025,10,28)).climateList(List.of()).build(); + News n2 = News.builder().newsUrl("u2").imageUrl("i2").newsTitle("t2") + .newsBody("b2").newsDate(LocalDate.of(2025,10,27)).climateList(List.of()).build(); + + when(newsPinRepository.findNewsByPinId(anyLong())).thenReturn(List.of(n1, n2)); when(climateRepository.findClimatesByNews(any())).thenReturn(List.of()); - GetPinListResponse resp = service.getPinList(null, null, 10L); + GetPinInfoResponse resp = service.getPinInfo(99L); - assertThat(resp.pinList()).hasSize(1); - assertThat(resp.pinList().get(0).region()).isEqualTo("Seoul"); + assertThat(resp.newsList()).hasSize(2); + NewsInfo first = resp.newsList().get(0); + assertThat(first.newsTitle()).isEqualTo("t1"); + assertThat(first.newsDate()).isEqualTo("2025-10-28"); } @Test - @DisplayName("잘못된 파라미터 조합 – InvalidParamException") - void getPinList_invalidParams() { - assertThatThrownBy(() -> service.getPinList("Seoul", "TEMPERATURE_RISE", null)) - .isInstanceOf(InvalidParamException.class); - assertThatThrownBy(() -> service.getPinList("Seoul", null, 1L)) - .isInstanceOf(InvalidParamException.class); - assertThatThrownBy(() -> service.getPinList(null, "TEMPERATURE_RISE", 1L)) - .isInstanceOf(InvalidParamException.class); - } - - @Test - @DisplayName("뉴스 없는 핀 – createPinInfo 내 예외 흐름") - void createPinInfo_noNews() { - when(pinRepository.findAll()).thenReturn(List.of(pin1)); - // 중요: any()로 null 매칭 - when(newsPinRepository.findNewsByPinId(any())).thenReturn(List.of()); - - assertThatThrownBy(() -> service.getPinList(null, null, null)) + @DisplayName("뉴스 없으면 PinNewsNotFoundException") + void getPinInfo_empty() { + when(newsPinRepository.findNewsByPinId(anyLong())).thenReturn(List.of()); + assertThatThrownBy(() -> service.getPinInfo(1L)) .isInstanceOf(PinNewsNotFoundException.class); } } \ No newline at end of file diff --git a/src/test/java/Konkuk/U2E/domain/pin/service/PinListServiceTest.java b/src/test/java/Konkuk/U2E/domain/pin/service/PinListServiceTest.java index d0c1af5..51efd3e 100644 --- a/src/test/java/Konkuk/U2E/domain/pin/service/PinListServiceTest.java +++ b/src/test/java/Konkuk/U2E/domain/pin/service/PinListServiceTest.java @@ -1,60 +1,224 @@ package Konkuk.U2E.domain.pin.service; +import Konkuk.U2E.domain.news.domain.Climate; +import Konkuk.U2E.domain.news.domain.ClimateProblem; import Konkuk.U2E.domain.news.domain.News; import Konkuk.U2E.domain.news.repository.ClimateRepository; import Konkuk.U2E.domain.news.repository.NewsPinRepository; -import Konkuk.U2E.domain.pin.dto.response.GetPinInfoResponse; -import Konkuk.U2E.domain.pin.dto.response.NewsInfo; +import Konkuk.U2E.domain.pin.domain.Pin; +import Konkuk.U2E.domain.pin.domain.Region; +import Konkuk.U2E.domain.pin.dto.response.GetPinListResponse; +import Konkuk.U2E.domain.pin.dto.response.PinInfo; +import Konkuk.U2E.domain.pin.exception.InvalidParamException; +import Konkuk.U2E.domain.pin.exception.NewsPinNotFoundException; import Konkuk.U2E.domain.pin.exception.PinNewsNotFoundException; +import Konkuk.U2E.domain.pin.repository.PinRepository; +import Konkuk.U2E.domain.pin.repository.RegionRepository; +import Konkuk.U2E.global.util.DateUtil; import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.when; @ActiveProfiles("test") -@SpringBootTest(classes = PinInfoService.class) -class PinInfoServiceTest { +@SpringBootTest(classes = PinListService.class) +class PinListServiceTest { - @MockitoBean NewsPinRepository newsPinRepository; @MockitoBean ClimateRepository climateRepository; + @MockitoBean NewsPinRepository newsPinRepository; + @MockitoBean PinRepository pinRepository; + @MockitoBean RegionRepository regionRepository; + @MockitoBean DateUtil dateUtil; + + @Resource PinListService service; + + Region seoul; + Region busan; + Pin pin1; + Pin pin2; + + News n1; + News n2; + News n3; + + @BeforeEach + @SuppressWarnings("unchecked") + void setUp() { + seoul = Region.builder() + .name("Seoul") + .latitude(new BigDecimal("37.5665")) + .longitude(new BigDecimal("126.9780")) + .build(); - @Resource - PinInfoService service; + busan = Region.builder() + .name("Busan") + .latitude(new BigDecimal("35.1796")) + .longitude(new BigDecimal("129.0756")) + .build(); + + // 테스트에서는 pinId를 주입하지 않음(null) → stubbing 시 any() 사용 + pin1 = Pin.builder().region(seoul).build(); + pin2 = Pin.builder().region(busan).build(); + + n1 = News.builder() + .newsUrl("u1").imageUrl("i1").newsTitle("t1") + .newsBody("b1").newsDate(LocalDate.of(2025,10,20)).climateList(List.of()) + .build(); + n2 = News.builder() + .newsUrl("u2").imageUrl("i2").newsTitle("t2") + .newsBody("b2").newsDate(LocalDate.of(2025,10,18)).climateList(List.of()) + .build(); + n3 = News.builder() + .newsUrl("u3").imageUrl("i3").newsTitle("t3") + .newsBody("b3").newsDate(LocalDate.of(2025,10,10)).climateList(List.of()) + .build(); + + // 공통 스텁(최신 뉴스 = 전달된 리스트의 첫 요소, 최근 여부 true) + when(dateUtil.getLatestNews(anyList())) + .thenAnswer(inv -> ((List) inv.getArgument(0)).get(0)); + when(dateUtil.checkLately(any())).thenReturn(true); + when(climateRepository.findClimatesByNews(any())) + .thenReturn(List.of( + Climate.builder().climateProblem(ClimateProblem.TEMPERATURE_RISE).news(n1).build() + )); + } @Test - @DisplayName("핀에 연결된 뉴스가 있으면 DTO로 매핑하여 반환") - void getPinInfo_success() { - News n1 = News.builder().newsUrl("u1").imageUrl("i1").newsTitle("t1") - .newsBody("b1").newsDate(LocalDate.of(2025,10,28)).climateList(List.of()).build(); - News n2 = News.builder().newsUrl("u2").imageUrl("i2").newsTitle("t2") - .newsBody("b2").newsDate(LocalDate.of(2025,10,27)).climateList(List.of()).build(); + @DisplayName("1) 전체 핀 – 모든 파라미터 null → getPinListByAll() 경로") + void getPinList_all_ok() { + when(pinRepository.findAll()).thenReturn(List.of(pin1, pin2)); + // pinId가 null이므로 any()를 사용해 null 호출도 매칭 + when(newsPinRepository.findNewsByPinId(any())) + .thenReturn(List.of(n1)) + .thenReturn(List.of(n2)); - when(newsPinRepository.findNewsByPinId(anyLong())).thenReturn(List.of(n1, n2)); - when(climateRepository.findClimatesByNews(any())).thenReturn(List.of()); + GetPinListResponse resp = service.getPinList(null, null, null); - GetPinInfoResponse resp = service.getPinInfo(99L); + assertThat(resp.pinList()).hasSize(2); + assertThat(resp.pinList().get(0).region()).isEqualTo("Seoul"); + assertThat(resp.pinList().get(1).region()).isEqualTo("Busan"); + assertThat(resp.pinList().get(0).isLately()).isTrue(); + } - assertThat(resp.newsList()).hasSize(2); - NewsInfo first = resp.newsList().get(0); - assertThat(first.newsTitle()).isEqualTo("t1"); - assertThat(first.newsDate()).isEqualTo("2025-10-28"); + @Test + @DisplayName("2) 지역 필터 – region=Seoul → getPinListByRegion() 경로") + void getPinList_region_ok() { + when(regionRepository.findRegionsByName(eq("Seoul"))).thenReturn(List.of(seoul)); + when(pinRepository.findPinByRegion(seoul)).thenReturn(pin1); + when(newsPinRepository.findNewsByPinId(any())).thenReturn(List.of(n2)); // latest=n2 + + GetPinListResponse resp = service.getPinList("Seoul", null, null); + + assertThat(resp.pinList()).hasSize(1); + PinInfo info = resp.pinList().get(0); + assertThat(info.region()).isEqualTo("Seoul"); + assertThat(info.isLately()).isTrue(); } @Test - @DisplayName("뉴스 없으면 PinNewsNotFoundException") - void getPinInfo_empty() { - when(newsPinRepository.findNewsByPinId(anyLong())).thenReturn(List.of()); - assertThatThrownBy(() -> service.getPinInfo(1L)) + @DisplayName("3) 기후 필터 – climate=TEMPERATURE_RISE → getPinListByClimate() 경로") + void getPinList_climate_ok() { + ClimateProblem cp = ClimateProblem.TEMPERATURE_RISE; + + // 이 기후에 해당하는 뉴스 목록 + when(climateRepository.findNewsByClimateProblem(cp)).thenReturn(List.of(n1, n2)); + + // 각 뉴스에 매핑된 핀 + when(newsPinRepository.findPinsByNews(n1)).thenReturn(List.of(pin1)); + when(newsPinRepository.findPinsByNews(n2)).thenReturn(List.of(pin1, pin2)); + + // 필터 통과 + when(climateRepository.existsByNewsAndClimateProblem(any(), eq(cp))).thenReturn(true); + + // 각 핀을 다시 latest 판단하기 위한 뉴스 조회 + when(newsPinRepository.findNewsByPinId(any())) + .thenReturn(List.of(n1, n2)) // for pin1 + .thenReturn(List.of(n2)); // for pin2 + + // climates 조회는 setUp()의 공통 스텁 사용 + + GetPinListResponse resp = service.getPinList(null, cp.name(), null); + + assertThat(resp.pinList()).isNotEmpty(); + // distinct로 인해 개수는 상황에 따라 다를 수 있으므로, 핵심 값만 검증 + assertThat(resp.pinList().stream().map(PinInfo::region)) + .contains("Seoul"); + } + + @Test + @DisplayName("3-1) 기후 필터 – 필터링 결과 중 일부만 통과(빈 리스트 아님)") + void getPinList_climate_filtered_ok() { + ClimateProblem cp = ClimateProblem.FINE_DUST; + + when(climateRepository.findNewsByClimateProblem(cp)).thenReturn(List.of(n1, n2, n3)); + when(newsPinRepository.findPinsByNews(n1)).thenReturn(List.of(pin1)); + when(newsPinRepository.findPinsByNews(n2)).thenReturn(List.of(pin1)); + when(newsPinRepository.findPinsByNews(n3)).thenReturn(List.of(pin2)); + + // n1만 cp 포함, 나머지는 제외 → latest는 n1만 남게 구성 + when(climateRepository.existsByNewsAndClimateProblem(eq(n1), eq(cp))).thenReturn(true); + when(climateRepository.existsByNewsAndClimateProblem(eq(n2), eq(cp))).thenReturn(false); + when(climateRepository.existsByNewsAndClimateProblem(eq(n3), eq(cp))).thenReturn(false); + + // pin1 처리 시: findNewsByPinId → n1, n2 (필터 후 n1만 전달되어 latest=n1) + when(newsPinRepository.findNewsByPinId(any())) + .thenReturn(List.of(n1, n2)) // for pin1 + .thenReturn(List.of(n3)); // for pin2 (필터에서 제거되어 호출되더라도 latest는 안전하게 동작) + + assertThatThrownBy(() -> service.getPinList(null, cp.name(), null)) + .isInstanceOf(PinNewsNotFoundException.class); + } + + @Test + @DisplayName("4) 최신 뉴스 카드 – newsId=10 → getPinListByNewsId() 경로") + void getPinList_newsId_ok() { + when(newsPinRepository.findPinsByNewsId(eq(10L))).thenReturn(List.of(pin1)); + when(newsPinRepository.findNewsByPinId(any())).thenReturn(List.of(n1)); + + GetPinListResponse resp = service.getPinList(null, null, 10L); + + assertThat(resp.pinList()).hasSize(1); + assertThat(resp.pinList().get(0).region()).isEqualTo("Seoul"); + } + + @Test + @DisplayName("4-1) 최신 뉴스 카드 – 해당 뉴스에 핀이 없으면 예외") + void getPinList_newsId_emptyPins_throws() { + when(newsPinRepository.findPinsByNewsId(eq(11L))).thenReturn(List.of()); + + assertThatThrownBy(() -> service.getPinList(null, null, 11L)) + .isInstanceOf(NewsPinNotFoundException.class); + } + + @Test + @DisplayName("예외 경로 – 파라미터 조합이 잘못되면 InvalidParamException") + void getPinList_invalidParams() { + assertThatThrownBy(() -> service.getPinList("Seoul", "TEMPERATURE_RISE", null)) + .isInstanceOf(InvalidParamException.class); + assertThatThrownBy(() -> service.getPinList("Seoul", null, 1L)) + .isInstanceOf(InvalidParamException.class); + assertThatThrownBy(() -> service.getPinList(null, "TEMPERATURE_RISE", 1L)) + .isInstanceOf(InvalidParamException.class); + } + + @Test + @DisplayName("예외 경로 – createPinInfo: 해당 핀에 뉴스가 없으면 PinNewsNotFoundException") + void createPinInfo_noNews_throws() { + when(pinRepository.findAll()).thenReturn(List.of(pin1)); + when(newsPinRepository.findNewsByPinId(any())).thenReturn(List.of()); + + assertThatThrownBy(() -> service.getPinList(null, null, null)) .isInstanceOf(PinNewsNotFoundException.class); } } \ No newline at end of file From 63a580ac3ae165a27fa87d5d858dec487460242b Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 30 Oct 2025 01:07:07 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[refactor]=20=EC=A1=B0=EA=B1=B4=20=EB=B6=84?= =?UTF-8?q?=EA=B8=B0=20=EC=88=98=EC=A0=95=20(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Konkuk/U2E/domain/pin/service/PinListService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/Konkuk/U2E/domain/pin/service/PinListService.java b/src/main/java/Konkuk/U2E/domain/pin/service/PinListService.java index a1cdbef..7d53e98 100644 --- a/src/main/java/Konkuk/U2E/domain/pin/service/PinListService.java +++ b/src/main/java/Konkuk/U2E/domain/pin/service/PinListService.java @@ -52,7 +52,7 @@ public GetPinListResponse getPinList(String region, String climate, Long newsId) ClimateProblem climateProblem = ClimateProblem.fromString(climate); return GetPinListResponse.of(getPinListByClimate(climateProblem)); } - if (region == null && climate == null && newsId != null) { + if (region == null && climate == null) { return GetPinListResponse.of(getPinListByNewsId(newsId)); }