1+ package BE_Elixir .Elixir .domain .recommendation .service ;
2+
3+ import BE_Elixir .Elixir .domain .ingredient .entity .Ingredient ;
4+ import BE_Elixir .Elixir .domain .ingredient .repository .IngredientRepository ;
5+ import BE_Elixir .Elixir .domain .member .entity .Member ;
6+ import BE_Elixir .Elixir .domain .recipe .dto .MaterialDTO ;
7+ import BE_Elixir .Elixir .domain .recipe .dto .request .RecipeRequestDTO ;
8+ import BE_Elixir .Elixir .domain .recipe .entity .Recipe ;
9+ import BE_Elixir .Elixir .domain .recipe .entity .RecipeIngredient ;
10+ import BE_Elixir .Elixir .domain .recipe .repository .RecipeEventRepository ;
11+ import BE_Elixir .Elixir .domain .recipe .repository .RecipeRepository ;
12+ import BE_Elixir .Elixir .domain .recommendation .dto .RecommendationResponseDTO ;
13+ import BE_Elixir .Elixir .global .enums .CategorySlowAging ;
14+ import BE_Elixir .Elixir .global .enums .CategoryType ;
15+ import BE_Elixir .Elixir .global .enums .Difficulty ;
16+ import BE_Elixir .Elixir .global .redis .RedisRecipeService ;
17+ import org .junit .jupiter .api .BeforeEach ;
18+ import org .junit .jupiter .api .DisplayName ;
19+ import org .junit .jupiter .api .Nested ;
20+ import org .junit .jupiter .api .Test ;
21+ import org .junit .jupiter .api .extension .ExtendWith ;
22+ import org .mockito .InjectMocks ;
23+ import org .mockito .Mock ;
24+ import org .mockito .Mockito ;
25+ import org .mockito .junit .jupiter .MockitoExtension ;
26+ import org .springframework .data .domain .PageRequest ;
27+ import org .springframework .data .domain .Pageable ;
28+
29+ import java .time .Duration ;
30+ import java .util .*;
31+
32+ import static org .assertj .core .api .Assertions .*;
33+ import static org .mockito .ArgumentMatchers .*;
34+ import static org .mockito .BDDMockito .*;
35+
36+
37+ @ ExtendWith (MockitoExtension .class )
38+ @ DisplayName ("RecommendationService 테스트" )
39+ class RecommendationServiceTest {
40+ @ InjectMocks
41+ RecommendationService recommendationService ;
42+
43+ @ Mock RecipeRepository recipeRepository ;
44+ @ Mock RedisRecipeService redisRecipeService ;
45+ @ Mock RecipeEventRepository recipeEventRepository ;
46+ @ Mock IngredientRepository ingredientRepository ;
47+
48+ private RecipeRequestDTO requestDTO ;
49+ private Recipe recipe ;
50+ private Member member ;
51+ private Pageable pageable ;
52+
53+ @ BeforeEach
54+ void setUp () {
55+ member = Member .builder ()
56+ .id (1L )
57+ .email ("test@example.com" )
58+ .nickname ("testUser" )
59+ .build ();
60+
61+ member .setMealStyle_채소위주 (true );
62+ member .setAllergy_밀 (true );
63+ member .setRecipeStyle_한식 (true );
64+ member .setReason_항산화강화 (true );
65+
66+ pageable = PageRequest .of (0 , 10 );
67+
68+ requestDTO = createSampleRecipeRequestDTO ();
69+
70+ recipe = Recipe .from (requestDTO , member );
71+ Ingredient ingredient = new Ingredient ();
72+ ingredient .setId (10L );
73+ ingredient .setName ("감자" );
74+
75+ RecipeIngredient recipeIngredient = new RecipeIngredient (recipe , ingredient );
76+ recipe .setIngredientTags (List .of (recipeIngredient ));
77+ }
78+
79+ private RecipeRequestDTO createSampleRecipeRequestDTO () {
80+ RecipeRequestDTO dto = new RecipeRequestDTO ();
81+ dto .setTitle ("감자조림" );
82+ dto .setDescription ("감자조림에 대한 설명" );
83+ dto .setCategoryType (CategoryType .한식 );
84+ dto .setCategorySlowAging (CategorySlowAging .항산화강화 );
85+ dto .setDifficulty (Difficulty .보통 );
86+ dto .setTimeHours (1 );
87+ dto .setTimeMinutes (20 );
88+ dto .setIngredientTagIds (List .of (10L ));
89+
90+ MaterialDTO ingredient = new MaterialDTO ("감자" , "2" , "개" );
91+ MaterialDTO seasoning1 = new MaterialDTO ("간장" , "2" , "큰술" );
92+ MaterialDTO seasoning2 = new MaterialDTO ("설탕" , "1" , "큰술" );
93+ dto .setIngredients (List .of (ingredient ));
94+ dto .setSeasonings (List .of (seasoning1 , seasoning2 ));
95+
96+ dto .setStepDescriptions (List .of ("1단계" ));
97+ dto .setTips ("팁" );
98+ dto .setAllergies (List .of ("우유" , "밀" ));
99+
100+ return dto ;
101+ }
102+
103+ @ Nested
104+ @ DisplayName ("사용자 맞춤형 레시피 추천 테스트" )
105+ class GetRecommendationsForUserTests {
106+
107+ @ Test
108+ @ DisplayName ("성공: 캐시에서 추천 결과가 존재하면 캐시 반환" )
109+ void returnsCachedRecommendations () {
110+ // given
111+ List <RecommendationResponseDTO > cached = List .of (
112+ new RecommendationResponseDTO (new Recipe (), false )
113+ );
114+ given (redisRecipeService .getCachedRecommendations (member .getId ())).willReturn (cached );
115+
116+ // when
117+ List <RecommendationResponseDTO > result = recommendationService .getRecommendationsForUser (member );
118+
119+ // then
120+ assertThat (result ).isEqualTo (cached );
121+ then (redisRecipeService ).should (times (1 )).getCachedRecommendations (member .getId ());
122+ then (recipeRepository ).should (never ()).findAll ();
123+ }
124+
125+ @ Test
126+ @ DisplayName ("성공: 캐시 없고, 필터링 후 추천 결과 반환 및 캐싱" )
127+ void returnsFilteredRecommendationsAndCaches () {
128+ // given
129+ given (redisRecipeService .getCachedRecommendations (member .getId ())).willReturn (null );
130+
131+ Recipe recipe1 = Mockito .mock (Recipe .class );
132+ given (recipe1 .getId ()).willReturn (10L );
133+ given (recipe1 .getAllergyList ()).willReturn (List .of ("우유" ));
134+ given (recipe1 .getIngredientTags ()).willReturn (List .of ());
135+ given (recipe1 .getCategoryType ()).willReturn (CategoryType .한식 );
136+ given (recipe1 .getCategorySlowAging ()).willReturn (CategorySlowAging .항산화강화 );
137+
138+ List <Recipe > allRecipes = List .of (recipe1 );
139+ given (recipeRepository .findAll ()).willReturn (allRecipes );
140+
141+ given (recipeEventRepository .existsByRecipeIdAndMemberIdAndScrapFlagTrue (10L , member .getId ())).willReturn (true );
142+
143+ // when
144+ List <RecommendationResponseDTO > result = recommendationService .getRecommendationsForUser (member );
145+
146+ // then
147+ assertThat (result ).hasSize (1 );
148+ assertThat (result .get (0 ).getScrappedByCurrentUser ()).isTrue ();
149+
150+ then (redisRecipeService ).should ().cacheRecommendations (eq (member .getId ()), anyList (), eq (Duration .ofHours (1 )));
151+ }
152+
153+ @ Test
154+ @ DisplayName ("성공: 필터링된 결과 없으면 전체 중 랜덤 3개 추천" )
155+ void returnsRandomWhenNoFiltered () {
156+ // given
157+ given (redisRecipeService .getCachedRecommendations (member .getId ())).willReturn (null );
158+
159+ Recipe recipe1 = new Recipe ();
160+ recipe1 .setCategoryType (CategoryType .한식 );
161+ recipe1 .setCategorySlowAging (CategorySlowAging .항산화강화 );
162+ recipe1 .setIngredientTags (new ArrayList <>());
163+
164+ Recipe recipe2 = new Recipe ();
165+ recipe2 .setCategoryType (CategoryType .중식 );
166+ recipe2 .setCategorySlowAging (CategorySlowAging .항산화강화 );
167+ recipe2 .setIngredientTags (new ArrayList <>());
168+
169+ Recipe recipe3 = new Recipe ();
170+ recipe3 .setCategoryType (CategoryType .디저트 );
171+ recipe3 .setCategorySlowAging (CategorySlowAging .혈당조절 );
172+ recipe3 .setIngredientTags (new ArrayList <>());
173+
174+ Recipe recipe4 = new Recipe ();
175+ recipe4 .setCategoryType (CategoryType .양식 );
176+ recipe4 .setCategorySlowAging (CategorySlowAging .혈당조절 );
177+ recipe4 .setIngredientTags (new ArrayList <>());
178+ List <Recipe > allRecipes = List .of (recipe1 , recipe2 , recipe3 , recipe4 );
179+
180+ given (recipeRepository .findAll ()).willReturn (allRecipes );
181+
182+ // when
183+ List <RecommendationResponseDTO > result = recommendationService .getRecommendationsForUser (member );
184+
185+ // then
186+ assertThat (result ).hasSizeLessThanOrEqualTo (3 );
187+ then (redisRecipeService ).should ().cacheRecommendations (eq (member .getId ()), anyList (), eq (Duration .ofHours (1 )));
188+ }
189+
190+ @ Test
191+ @ DisplayName ("예외: 캐시 조회 후 결과가 null 이거나 비어있으면 빈 리스트 반환" )
192+ void getRecommendedKeywords_ReturnsEmptyWhenCacheEmpty () {
193+ // given
194+ given (redisRecipeService .getCachedRecommendations (member .getId ())).willReturn (null );
195+
196+ // when
197+ List <String > keywords = recommendationService .getRecommendedKeywords (member );
198+
199+ // then
200+ assertThat (keywords ).isEmpty ();
201+ }
202+ }
203+
204+
205+ @ Nested
206+ @ DisplayName ("추천 검색어 조회 테스트" )
207+ class GetRecommendedKeywordsTests {
208+
209+ @ Test
210+ @ DisplayName ("성공: 캐시된 추천 결과가 있고, 키워드 및 식재료 이름 포함 반환" )
211+ void returnsKeywordsFromCachedRecommendations () {
212+ // given
213+ RecommendationResponseDTO dto = Mockito .mock (RecommendationResponseDTO .class );
214+ given (dto .getTitle ()).willReturn ("감자조림은 맛있다" );
215+ given (dto .getIngredientTagIds ()).willReturn (List .of (1L , 2L ));
216+
217+ given (redisRecipeService .getCachedRecommendations (member .getId ()))
218+ .willReturn (List .of (dto ));
219+
220+ Ingredient ing1 = new Ingredient ();
221+ ing1 .setName ("감자" );
222+ Ingredient ing2 = new Ingredient ();
223+ ing2 .setName ("고추" );
224+
225+ given (ingredientRepository .findByIdIn (List .of (1L , 2L ))).willReturn (List .of (ing1 , ing2 ));
226+
227+ // when
228+ List <String > keywords = recommendationService .getRecommendedKeywords (member );
229+
230+ // then
231+ assertThat (keywords ).contains ("감자조림" , "맛있다" , "감자" , "고추" );
232+ assertThat (keywords .size ()).isLessThanOrEqualTo (5 );
233+ }
234+ }
235+
236+ }
0 commit comments