From 217dbd794707c1508963ffa014d166a0fdcdf5ab Mon Sep 17 00:00:00 2001 From: Kang Dong Hyeon Date: Sat, 14 Jun 2025 18:40:03 +0900 Subject: [PATCH] =?UTF-8?q?test:=20=ED=9A=8C=EC=9B=90=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MemberRepository 테스트 케이스 추가 (이메일 조회, 중복 검증, OAuth2 조회) - Subscription 연관관계 테스트 (cascade, orphanRemoval) - JPA Auditing 테스트 (생성/수정 시간 자동 설정) - H2 데이터베이스 설정 추가 (테스트 환경) - 테스트용 JPA Auditing 설정 분리 --- account-service/build.gradle | 2 + .../account_service/domain/Member.java | 15 +- .../account_service/domain/Subscription.java | 5 + .../repository/MemberRepository.java | 12 ++ .../repository/SubscriptionRepository.java | 8 + .../AccountServiceApplicationTests.java | 8 - .../synapse/account_service/TestConfig.java | 12 ++ .../config/TestJpaAuditingConfig.java | 19 ++ .../repository/MemberRepositoryTest.java | 184 ++++++++++++++++++ .../src/test/resources/application-test.yml | 35 ++++ .../src/test/resources/application.yml | 33 +--- 11 files changed, 289 insertions(+), 44 deletions(-) create mode 100644 account-service/src/main/java/com/synapse/account_service/repository/MemberRepository.java create mode 100644 account-service/src/main/java/com/synapse/account_service/repository/SubscriptionRepository.java delete mode 100644 account-service/src/test/java/com/synapse/account_service/AccountServiceApplicationTests.java create mode 100644 account-service/src/test/java/com/synapse/account_service/TestConfig.java create mode 100644 account-service/src/test/java/com/synapse/account_service/config/TestJpaAuditingConfig.java create mode 100644 account-service/src/test/java/com/synapse/account_service/repository/MemberRepositoryTest.java create mode 100644 account-service/src/test/resources/application-test.yml diff --git a/account-service/build.gradle b/account-service/build.gradle index ec72aed..a03aef4 100644 --- a/account-service/build.gradle +++ b/account-service/build.gradle @@ -38,6 +38,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + //H2 Database + implementation 'com.h2database:h2' } tasks.named('test') { diff --git a/account-service/src/main/java/com/synapse/account_service/domain/Member.java b/account-service/src/main/java/com/synapse/account_service/domain/Member.java index 97c305a..0303a9f 100644 --- a/account-service/src/main/java/com/synapse/account_service/domain/Member.java +++ b/account-service/src/main/java/com/synapse/account_service/domain/Member.java @@ -1,17 +1,15 @@ package com.synapse.account_service.domain; -import java.util.List; - -import org.springframework.security.core.GrantedAuthority; - import com.synapse.account_service.common.BaseEntity; import com.synapse.account_service.domain.enums.MemberRole; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; +import lombok.Getter; import lombok.NoArgsConstructor; +@Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "members") @@ -53,5 +51,14 @@ public Member(String username, String password, String email, String provider, S this.email = email; this.provider = provider; this.picture = picture; + this.registrationId = registrationId; + this.role = role; + } + + public void setSubscription(Subscription subscription) { + this.subscription = subscription; + if (subscription != null) { + subscription.setMemberInternal(this); // 무한 루프 방지를 위해 내부 메서드 호출 + } } } diff --git a/account-service/src/main/java/com/synapse/account_service/domain/Subscription.java b/account-service/src/main/java/com/synapse/account_service/domain/Subscription.java index 3524b03..cf705ed 100644 --- a/account-service/src/main/java/com/synapse/account_service/domain/Subscription.java +++ b/account-service/src/main/java/com/synapse/account_service/domain/Subscription.java @@ -8,6 +8,7 @@ import jakarta.persistence.*; import lombok.*; +@Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "subscription") @@ -34,4 +35,8 @@ public Subscription(Member member, SubscriptionTier tier, ZonedDateTime nextRene this.tier = tier; this.nextRenewalDate = nextRenewalDate; } + + protected void setMemberInternal(Member member) { + this.member = member; + } } diff --git a/account-service/src/main/java/com/synapse/account_service/repository/MemberRepository.java b/account-service/src/main/java/com/synapse/account_service/repository/MemberRepository.java new file mode 100644 index 0000000..7f2f3af --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/repository/MemberRepository.java @@ -0,0 +1,12 @@ +package com.synapse.account_service.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.synapse.account_service.domain.Member; + +public interface MemberRepository extends JpaRepository { + Optional findByEmail(String email); + Optional findByProviderAndRegistrationId(String provider, String registrationId); +} diff --git a/account-service/src/main/java/com/synapse/account_service/repository/SubscriptionRepository.java b/account-service/src/main/java/com/synapse/account_service/repository/SubscriptionRepository.java new file mode 100644 index 0000000..ce1db9c --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/repository/SubscriptionRepository.java @@ -0,0 +1,8 @@ +package com.synapse.account_service.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.synapse.account_service.domain.Subscription; + +public interface SubscriptionRepository extends JpaRepository { +} diff --git a/account-service/src/test/java/com/synapse/account_service/AccountServiceApplicationTests.java b/account-service/src/test/java/com/synapse/account_service/AccountServiceApplicationTests.java deleted file mode 100644 index df011e2..0000000 --- a/account-service/src/test/java/com/synapse/account_service/AccountServiceApplicationTests.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.synapse.account_service; - -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest(classes = AccountServiceApplication.class) -class AccountServiceApplicationTests { - -} diff --git a/account-service/src/test/java/com/synapse/account_service/TestConfig.java b/account-service/src/test/java/com/synapse/account_service/TestConfig.java new file mode 100644 index 0000000..55b385d --- /dev/null +++ b/account-service/src/test/java/com/synapse/account_service/TestConfig.java @@ -0,0 +1,12 @@ +package com.synapse.account_service; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@ActiveProfiles("test") +@SpringBootTest(classes = {AccountServiceApplication.class}) +public class TestConfig { + +} diff --git a/account-service/src/test/java/com/synapse/account_service/config/TestJpaAuditingConfig.java b/account-service/src/test/java/com/synapse/account_service/config/TestJpaAuditingConfig.java new file mode 100644 index 0000000..5d5f625 --- /dev/null +++ b/account-service/src/test/java/com/synapse/account_service/config/TestJpaAuditingConfig.java @@ -0,0 +1,19 @@ +package com.synapse.account_service.config; + +import java.util.Optional; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@TestConfiguration +@EnableJpaAuditing +public class TestJpaAuditingConfig { + + // @CreatedBy, @LastModifiedBy를 테스트하기 위해 임시 AuditorAware 빈을 등록합니다. + @Bean + public AuditorAware auditorProvider() { + return () -> Optional.of("test_user"); + } +} diff --git a/account-service/src/test/java/com/synapse/account_service/repository/MemberRepositoryTest.java b/account-service/src/test/java/com/synapse/account_service/repository/MemberRepositoryTest.java new file mode 100644 index 0000000..9138420 --- /dev/null +++ b/account-service/src/test/java/com/synapse/account_service/repository/MemberRepositoryTest.java @@ -0,0 +1,184 @@ +package com.synapse.account_service.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.ZonedDateTime; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.dao.DataIntegrityViolationException; + +import com.synapse.account_service.config.TestJpaAuditingConfig; +import com.synapse.account_service.domain.Member; +import com.synapse.account_service.domain.Subscription; +import com.synapse.account_service.domain.enums.MemberRole; +import com.synapse.account_service.domain.enums.SubscriptionTier; + +import jakarta.persistence.EntityManager; + + +@DataJpaTest +@Import(TestJpaAuditingConfig.class) +public class MemberRepositoryTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private SubscriptionRepository subscriptionRepository; + + @Autowired + private EntityManager entityManager; + + private Member testMember; + + @BeforeEach + void setUp() { + testMember = Member.builder() + .email("test@example.com") + .password("encrypted_password") + .username("테스트유저") + .role(MemberRole.USER) + .provider("local") + .build(); + } + + @Test + @DisplayName("새로운 회원을 저장하면, 연관된 구독 정보도 함께 저장되어야 한다 (cascade)") + void saveMemberWithSubscription_shouldSaveBoth() { + // given: 테스트할 새로운 회원과 구독 정보를 준비합니다. + Subscription newSubscription = Subscription.builder() + .tier(SubscriptionTier.FREE) + .nextRenewalDate(ZonedDateTime.now().plusMonths(1)) + .build(); + + // 연관관계 편의 메서드를 사용하여 두 엔티티를 연결합니다. + testMember.setSubscription(newSubscription); + + // when: Member만 저장합니다. Subscription은 cascade 옵션에 따라 함께 저장되어야 합니다. + Member savedMember = memberRepository.save(testMember); + + // then: 결과를 검증합니다. + assertThat(savedMember.getId()).isNotNull(); + assertThat(savedMember.getSubscription()).isNotNull(); + assertThat(savedMember.getSubscription().getId()).isNotNull(); + assertThat(savedMember.getSubscription().getTier()).isEqualTo(SubscriptionTier.FREE); + + // DB에 실제로 두 엔티티가 모두 저장되었는지 확인합니다. + assertThat(memberRepository.count()).isEqualTo(1); + assertThat(subscriptionRepository.count()).isEqualTo(1); + } + + @Test + @DisplayName("이메일로 회원을 성공적으로 조회해야 한다") + void findByEmail_shouldReturnMember() { + // given: 먼저 테스트할 회원을 저장합니다. + memberRepository.save(testMember); + + // when: 저장된 이메일로 조회를 시도합니다. + Optional foundMemberOpt = memberRepository.findByEmail("test@example.com"); + + // then: 결과가 존재하고, 이메일이 일치하는지 확인합니다. + assertThat(foundMemberOpt).isPresent(); + assertThat(foundMemberOpt.get().getEmail()).isEqualTo("test@example.com"); + } + + @Test + @DisplayName("중복된 이메일로 회원을 저장하려고 하면, DataIntegrityViolationException이 발생해야 한다") + void save_withDuplicateEmail_shouldThrowException() { + // given: 첫 번째 회원을 저장하고, DB에 즉시 반영(flush)합니다. + memberRepository.save(testMember); + entityManager.flush(); // 영속성 컨텍스트의 변경사항을 DB에 즉시 반영 + entityManager.clear(); // 1차 캐시를 비워서, 다음 조회가 DB에서 일어나도록 강제 + + // when: 동일한 이메일을 가진 새로운 회원을 만듭니다. + Member duplicateMember = Member.builder() + .email("test@example.com") // 중복된 이메일 + .password("another_password") + .role(MemberRole.USER) + .provider("local") + .build(); + + // then: 이 회원을 저장하려고 할 때, DB의 unique 제약 조건에 걸려 예외가 발생하는지 확인합니다. + assertThrows(DataIntegrityViolationException.class, () -> { + memberRepository.saveAndFlush(duplicateMember); // DB 제약조건을 바로 확인하기 위해 flush 사용 + }); + } + + @Test + @DisplayName("존재하지 않는 이메일로 조회하면, 비어있는 Optional을 반환해야 한다") + void findByEmail_withNonExistentEmail_shouldReturnEmpty() { + // when: 존재하지 않는 이메일로 조회를 시도합니다. + Optional foundMemberOpt = memberRepository.findByEmail("not-exist@example.com"); + + // then: 결과가 비어있는지 확인합니다. + assertThat(foundMemberOpt).isEmpty(); + } + + @Test + @DisplayName("회원의 구독 정보를 null로 설정하고 저장하면, 구독 정보가 삭제되어야 한다 (orphanRemoval)") + void removeSubscription_shouldDeleteSubscriptionEntity() { + // given: 회원과 구독 정보를 함께 저장합니다. + Subscription subscription = Subscription.builder() + .tier(SubscriptionTier.PRO) + .nextRenewalDate(ZonedDateTime.now()) + .build(); + + testMember.setSubscription(subscription); + + Member savedMember = memberRepository.save(testMember); + + // DB에 둘 다 존재하는지 먼저 확인 + assertThat(memberRepository.count()).isEqualTo(1); + assertThat(subscriptionRepository.count()).isEqualTo(1); + + // when: 회원의 구독 정보 참조를 제거합니다. + savedMember.setSubscription(null); + memberRepository.saveAndFlush(savedMember); // 변경사항을 즉시 DB에 반영 + + // then: Member는 남아있지만, 고아가 된 Subscription은 삭제되어야 합니다. + assertThat(memberRepository.count()).isEqualTo(1); + assertThat(subscriptionRepository.count()).isEqualTo(0); + } + + @Test + @DisplayName("엔티티 저장 시 생성 날짜(createdAt)가 자동으로 설정되어야 한다") + void save_shouldSetCreatedAt() { + // given + + // when + Member savedMember = memberRepository.saveAndFlush(testMember); + + // then + assertThat(savedMember.getCreatedDate()).isNotNull(); + assertThat(savedMember.getUpdatedDate()).isNotNull(); + } + + @Test + @DisplayName("Provider와 Provider ID로 회원을 성공적으로 조회해야 한다 (OAuth2)") + void findByProviderAndRegistrationId_shouldReturnMember() { + // given + Member oauthMember = Member.builder() + .email("google_user@example.com") + .password("social_login_password") // 실제로는 비밀번호가 없을 수도 있습니다. + .username("구글유저") + .role(MemberRole.USER) + .provider("google") // 소셜 로그인 제공자 + .registrationId("1234567890") // 제공자가 부여한 고유 ID + .build(); + memberRepository.save(oauthMember); + + // when + Optional foundMemberOpt = memberRepository.findByProviderAndRegistrationId("google", "1234567890"); + + // then + assertThat(foundMemberOpt).isPresent(); + assertThat(foundMemberOpt.get().getEmail()).isEqualTo("google_user@example.com"); + } +} diff --git a/account-service/src/test/resources/application-test.yml b/account-service/src/test/resources/application-test.yml new file mode 100644 index 0000000..b2a30b2 --- /dev/null +++ b/account-service/src/test/resources/application-test.yml @@ -0,0 +1,35 @@ +spring: + h2: + console: + enabled: true + datasource: + hikari: + driver-class-name: org.h2.Driver + jdbc-url: jdbc:h2:mem:test;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: + + jpa: + database-platform: org.hibernate.dialect.PostgreSQLDialect + properties: + hibernate: + format: + sql: true + highlight: + sql: true + hbm2ddl: + auto: create-drop + dialect: org.hibernate.dialect.PostgreSQLDialect + open-in-view: false + show-sql: true + +logging: + level: + org: + hibernate: + orm: + jdbc: + bind: info + spring: + transaction: + interceptor: info diff --git a/account-service/src/test/resources/application.yml b/account-service/src/test/resources/application.yml index f86dc9f..a498ef5 100644 --- a/account-service/src/test/resources/application.yml +++ b/account-service/src/test/resources/application.yml @@ -1,35 +1,4 @@ spring: profiles: active: test - h2: - console: - enabled: true - datasource: - hikari: - driver-class-name: org.h2.Driver - jdbc-url: jdbc:h2:mem:test - username: sa - password: - - jpa: - properties: - hibernate: - format: - sql: true - highlight: - sql: true - hbm2ddl: - auto: create-drop - open-in-view: false - show-sql: true - -logging: - level: - org: - hibernate: - orm: - jdbc: - bind: info - spring: - transaction: - interceptor: info + \ No newline at end of file