Skip to content

Commit cf7b075

Browse files
authored
[test/#411] Personalization/Recommendation 이벤트 경계 테스트 보강 (#417)
* test: 프로필 생성 이벤트 단위 테스트 보강 * test: 프로필 생성 이벤트 리스너 테스트 보강 * test: 프로필 생성 관련 통합 테스트 작성 * refactor: 이벤트 경계 검증 통합 테스트는 테스트 컨테이너를 띄우지 않고 진행
1 parent dd6d4f7 commit cf7b075

3 files changed

Lines changed: 232 additions & 2 deletions

File tree

src/test/java/com/techfork/domain/recommendation/listener/PersonalizedProfileGeneratedEventListenerTest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static org.mockito.ArgumentMatchers.eq;
2424
import static org.mockito.BDDMockito.given;
2525
import static org.mockito.Mockito.mock;
26+
import static org.mockito.Mockito.never;
2627
import static org.mockito.Mockito.verify;
2728
import static org.mockito.Mockito.verifyNoInteractions;
2829

@@ -39,8 +40,8 @@ class PersonalizedProfileGeneratedEventListenerTest {
3940
private PersonalizedProfileGeneratedEventListener listener;
4041

4142
@Test
42-
@DisplayName("프로필 생성 이벤트를 받으면 추천을 생성한다")
43-
void handle_GeneratesRecommendationsWhenProfileGeneratedEventIsReceived() {
43+
@DisplayName("프로필 생성 이벤트를 받으면 이벤트 스냅샷으로 추천을 생성한다")
44+
void handle_GeneratesRecommendationsWithEventSnapshotWhenProfileGeneratedEventIsReceived() {
4445
Long userId = 1L;
4546
User user = mock(User.class);
4647
float[] profileVector = new float[]{0.1f, 0.2f};
@@ -60,6 +61,7 @@ void handle_GeneratesRecommendationsWhenProfileGeneratedEventIsReceived() {
6061
argThat(vector -> Arrays.equals(vector, profileVector)),
6162
eq(keyKeywords)
6263
);
64+
verify(recommendationService, never()).generateRecommendationsForUser(user);
6365
}
6466

6567
@Test
@@ -86,6 +88,7 @@ void handle_RecommendationFailureDoesNotPropagateException() {
8688
argThat(vector -> Arrays.equals(vector, profileVector)),
8789
eq(keyKeywords)
8890
);
91+
verify(recommendationService, never()).generateRecommendationsForUser(user);
8992
}
9093

9194
@Test
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.techfork.personalization.application.event;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
11+
12+
class PersonalizedProfileGeneratedEventTest {
13+
14+
@Test
15+
@DisplayName("프로필 벡터는 생성 시점 값으로 복사된다")
16+
void constructor_CopiesProfileVector() {
17+
float[] profileVector = new float[]{0.1f, 0.2f};
18+
19+
PersonalizedProfileGeneratedEvent event = new PersonalizedProfileGeneratedEvent(
20+
1L,
21+
profileVector,
22+
List.of("Spring")
23+
);
24+
profileVector[0] = 9.9f;
25+
26+
assertThat(event.profileVector()).containsExactly(0.1f, 0.2f);
27+
}
28+
29+
@Test
30+
@DisplayName("프로필 벡터 조회 결과를 변경해도 이벤트 내부 상태는 변경되지 않는다")
31+
void profileVector_ReturnsCopy() {
32+
PersonalizedProfileGeneratedEvent event = new PersonalizedProfileGeneratedEvent(
33+
1L,
34+
new float[]{0.1f, 0.2f},
35+
List.of("Spring")
36+
);
37+
38+
float[] returnedVector = event.profileVector();
39+
returnedVector[0] = 9.9f;
40+
41+
assertThat(event.profileVector()).containsExactly(0.1f, 0.2f);
42+
}
43+
44+
@Test
45+
@DisplayName("핵심 키워드는 생성 시점 값으로 복사되고 불변 목록으로 유지된다")
46+
void constructor_CopiesKeyKeywordsAsImmutableList() {
47+
List<String> keyKeywords = new ArrayList<>(List.of("Spring", "JPA"));
48+
49+
PersonalizedProfileGeneratedEvent event = new PersonalizedProfileGeneratedEvent(
50+
1L,
51+
new float[]{0.1f},
52+
keyKeywords
53+
);
54+
keyKeywords.add("Kafka");
55+
56+
assertThat(event.keyKeywords()).containsExactly("Spring", "JPA");
57+
assertThatThrownBy(() -> event.keyKeywords().add("Kafka"))
58+
.isInstanceOf(UnsupportedOperationException.class);
59+
}
60+
61+
@Test
62+
@DisplayName("핵심 키워드가 null이면 빈 목록으로 정규화한다")
63+
void constructor_NullKeyKeywordsDefaultsToEmptyList() {
64+
PersonalizedProfileGeneratedEvent event = new PersonalizedProfileGeneratedEvent(
65+
1L,
66+
new float[]{0.1f},
67+
null
68+
);
69+
70+
assertThat(event.keyKeywords()).isEmpty();
71+
}
72+
73+
@Test
74+
@DisplayName("프로필 벡터가 null이면 null로 유지한다")
75+
void constructor_NullProfileVectorRemainsNull() {
76+
PersonalizedProfileGeneratedEvent event = new PersonalizedProfileGeneratedEvent(
77+
1L,
78+
null,
79+
List.of("Spring")
80+
);
81+
82+
assertThat(event.profileVector()).isNull();
83+
}
84+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package com.techfork.personalization.integration;
2+
3+
import com.techfork.domain.recommendation.listener.PersonalizedProfileGeneratedEventListener;
4+
import com.techfork.domain.recommendation.service.RecommendationService;
5+
import com.techfork.personalization.application.event.PersonalizedProfileGeneratedEvent;
6+
import com.techfork.useraccount.application.query.lookup.UserLookupService;
7+
import com.techfork.useraccount.domain.User;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.DisplayName;
10+
import org.junit.jupiter.api.Tag;
11+
import org.junit.jupiter.api.Test;
12+
import org.springframework.beans.factory.annotation.Autowired;
13+
import org.springframework.context.ApplicationEventPublisher;
14+
import org.springframework.context.annotation.Bean;
15+
import org.springframework.context.annotation.Configuration;
16+
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
17+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
18+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
19+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
20+
import org.springframework.transaction.PlatformTransactionManager;
21+
import org.springframework.transaction.annotation.EnableTransactionManagement;
22+
import org.springframework.transaction.support.TransactionTemplate;
23+
24+
import java.util.Arrays;
25+
import java.util.List;
26+
27+
import javax.sql.DataSource;
28+
29+
import static org.mockito.ArgumentMatchers.argThat;
30+
import static org.mockito.ArgumentMatchers.eq;
31+
import static org.mockito.BDDMockito.given;
32+
import static org.mockito.Mockito.mock;
33+
import static org.mockito.Mockito.never;
34+
import static org.mockito.Mockito.reset;
35+
import static org.mockito.Mockito.verify;
36+
import static org.mockito.Mockito.verifyNoInteractions;
37+
38+
@Tag("integration")
39+
@SpringJUnitConfig(PersonalizedProfileGeneratedAfterCommitIntegrationTest.TestConfig.class)
40+
class PersonalizedProfileGeneratedAfterCommitIntegrationTest {
41+
42+
@Autowired
43+
private ApplicationEventPublisher eventPublisher;
44+
45+
@Autowired
46+
private TransactionTemplate transactionTemplate;
47+
48+
@Autowired
49+
private UserLookupService userLookupService;
50+
51+
@Autowired
52+
private RecommendationService recommendationService;
53+
54+
@BeforeEach
55+
void setUp() {
56+
reset(userLookupService, recommendationService);
57+
}
58+
59+
@Test
60+
@DisplayName("프로필 생성 이벤트는 실제 트랜잭션 커밋 이후 추천 생성을 실행한다")
61+
void profileGeneratedEvent_CommittedTransaction_GeneratesRecommendationAfterCommit() {
62+
Long userId = 1L;
63+
User user = mock(User.class);
64+
float[] profileVector = new float[]{0.1f, 0.2f};
65+
List<String> keyKeywords = List.of("Spring", "JPA");
66+
given(userLookupService.getUserOrThrow(userId)).willReturn(user);
67+
given(recommendationService.generateRecommendationsForUser(
68+
eq(user),
69+
argThat(vector -> Arrays.equals(vector, profileVector)),
70+
eq(keyKeywords)
71+
)).willReturn(5);
72+
73+
transactionTemplate.executeWithoutResult(status -> {
74+
eventPublisher.publishEvent(new PersonalizedProfileGeneratedEvent(userId, profileVector, keyKeywords));
75+
76+
verifyNoInteractions(userLookupService, recommendationService);
77+
});
78+
79+
verify(userLookupService).getUserOrThrow(userId);
80+
verify(recommendationService).generateRecommendationsForUser(
81+
eq(user),
82+
argThat(vector -> Arrays.equals(vector, profileVector)),
83+
eq(keyKeywords)
84+
);
85+
verify(recommendationService, never()).generateRecommendationsForUser(user);
86+
}
87+
88+
@Test
89+
@DisplayName("트랜잭션이 롤백되면 프로필 생성 이벤트의 추천 후처리는 실행되지 않는다")
90+
void profileGeneratedEvent_RolledBackTransaction_DoesNotGenerateRecommendation() {
91+
transactionTemplate.executeWithoutResult(status -> {
92+
eventPublisher.publishEvent(new PersonalizedProfileGeneratedEvent(
93+
1L,
94+
new float[]{0.1f},
95+
List.of("Spring")
96+
));
97+
status.setRollbackOnly();
98+
});
99+
100+
verifyNoInteractions(userLookupService, recommendationService);
101+
}
102+
103+
@Configuration
104+
@EnableTransactionManagement
105+
static class TestConfig {
106+
107+
@Bean
108+
PersonalizedProfileGeneratedEventListener personalizedProfileGeneratedEventListener(
109+
UserLookupService userLookupService,
110+
RecommendationService recommendationService
111+
) {
112+
return new PersonalizedProfileGeneratedEventListener(userLookupService, recommendationService);
113+
}
114+
115+
@Bean
116+
UserLookupService userLookupService() {
117+
return mock(UserLookupService.class);
118+
}
119+
120+
@Bean
121+
RecommendationService recommendationService() {
122+
return mock(RecommendationService.class);
123+
}
124+
125+
@Bean
126+
DataSource dataSource() {
127+
return new EmbeddedDatabaseBuilder()
128+
.generateUniqueName(true)
129+
.setType(EmbeddedDatabaseType.H2)
130+
.build();
131+
}
132+
133+
@Bean
134+
PlatformTransactionManager transactionManager(DataSource dataSource) {
135+
return new DataSourceTransactionManager(dataSource);
136+
}
137+
138+
@Bean
139+
TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
140+
return new TransactionTemplate(transactionManager);
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)