From cd118e8ed3a6f1807f1dec8bce5e67ea71ced176 Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 15:06:00 +0900 Subject: [PATCH 1/3] =?UTF-8?q?test:=20=EC=88=9C=EC=88=98=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(PR=201/9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DefaultSurveyRefundPolicy: 환불 금액 계산 로직 8개 케이스 - QuestionConverter: DTO↔Entity 변환 로직 전 문항 타입 커버 (10개) - SurveyExportService: CSV 빌딩 로직 13개 케이스 (권한, BOM, 이스케이프 등) - DiscountCodeCommandService: 할인 코드 생성 및 중복 재시도 6개 케이스 --- gradlew | 0 .../DiscountCodeCommandServiceTest.java | 128 +++++++ .../service/QuestionConverterTest.java | 257 ++++++++++++++ .../export/SurveyExportServiceTest.java | 331 ++++++++++++++++++ .../refund/DefaultSurveyRefundPolicyTest.java | 156 +++++++++ 5 files changed, 872 insertions(+) mode change 100644 => 100755 gradlew create mode 100644 src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java create mode 100644 src/test/java/OneQ/OnSurvey/domain/survey/service/refund/DefaultSurveyRefundPolicyTest.java diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java new file mode 100644 index 00000000..975e1d41 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java @@ -0,0 +1,128 @@ +package OneQ.OnSurvey.domain.discount.service; + +import OneQ.OnSurvey.domain.discount.entity.DiscountCode; +import OneQ.OnSurvey.domain.discount.model.request.CreateDiscountCodeRequest; +import OneQ.OnSurvey.domain.discount.model.response.DiscountCodeResponse; +import OneQ.OnSurvey.domain.discount.repository.DiscountCodeRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class DiscountCodeCommandServiceTest { + + private static final Pattern CODE_PATTERN = Pattern.compile("[A-Z0-9]{6}"); + + @Mock + private DiscountCodeRepository discountCodeRepository; + + @InjectMocks + private DiscountCodeCommandService discountCodeCommandService; + + @Test + @DisplayName("할인 코드 생성 성공 - 6자리 대문자+숫자 코드 반환") + void create_success() { + CreateDiscountCodeRequest request = new CreateDiscountCodeRequest( + "OnSurvey", LocalDate.of(2026, 12, 31) + ); + given(discountCodeRepository.existsByCode(anyString())).willReturn(false); + given(discountCodeRepository.save(any(DiscountCode.class))) + .willAnswer(inv -> inv.getArgument(0)); + + DiscountCodeResponse response = discountCodeCommandService.create(request); + + assertThat(response.organizationName()).isEqualTo("OnSurvey"); + assertThat(response.code()).hasSize(6); + assertThat(CODE_PATTERN.matcher(response.code()).matches()).isTrue(); + assertThat(response.expiredAt()).isEqualTo(LocalDate.of(2026, 12, 31)); + } + + @Test + @DisplayName("코드 중복 시 재생성 후 저장") + void create_codeConflict_retries() { + CreateDiscountCodeRequest request = new CreateDiscountCodeRequest( + "TestOrg", LocalDate.of(2027, 1, 1) + ); + // 처음 두 번은 중복, 세 번째에 성공 + given(discountCodeRepository.existsByCode(anyString())) + .willReturn(true, true, false); + given(discountCodeRepository.save(any(DiscountCode.class))) + .willAnswer(inv -> inv.getArgument(0)); + + DiscountCodeResponse response = discountCodeCommandService.create(request); + + assertThat(response.code()).hasSize(6); + verify(discountCodeRepository, times(3)).existsByCode(anyString()); + } + + @Test + @DisplayName("저장된 엔티티의 조직명이 요청값과 동일") + void create_savedEntity_hasCorrectOrgName() { + String orgName = "학회명"; + LocalDate expiredAt = LocalDate.of(2026, 6, 30); + CreateDiscountCodeRequest request = new CreateDiscountCodeRequest(orgName, expiredAt); + + given(discountCodeRepository.existsByCode(anyString())).willReturn(false); + ArgumentCaptor captor = ArgumentCaptor.forClass(DiscountCode.class); + given(discountCodeRepository.save(captor.capture())) + .willAnswer(inv -> inv.getArgument(0)); + + discountCodeCommandService.create(request); + + DiscountCode saved = captor.getValue(); + assertThat(saved.getOrganizationName()).isEqualTo(orgName); + assertThat(saved.getExpiredAt()).isEqualTo(expiredAt); + assertThat(CODE_PATTERN.matcher(saved.getCode()).matches()).isTrue(); + } + + @RepeatedTest(20) + @DisplayName("반복 생성 시 매번 유효한 6자리 코드 생성") + void create_repeatedCalls_alwaysValidCode() { + CreateDiscountCodeRequest request = new CreateDiscountCodeRequest( + "RepeatedOrg", LocalDate.of(2027, 12, 31) + ); + given(discountCodeRepository.existsByCode(anyString())).willReturn(false); + given(discountCodeRepository.save(any(DiscountCode.class))) + .willAnswer(inv -> inv.getArgument(0)); + + DiscountCodeResponse response = discountCodeCommandService.create(request); + + assertThat(CODE_PATTERN.matcher(response.code()).matches()).isTrue(); + } + + @Test + @DisplayName("여러 번 생성 시 코드가 무작위로 다양하게 생성됨") + void create_multipleInvocations_producesDiverseCodes() { + CreateDiscountCodeRequest request = new CreateDiscountCodeRequest( + "Org", LocalDate.of(2027, 1, 1) + ); + given(discountCodeRepository.existsByCode(anyString())).willReturn(false); + given(discountCodeRepository.save(any(DiscountCode.class))) + .willAnswer(inv -> inv.getArgument(0)); + + Set codes = new HashSet<>(); + for (int i = 0; i < 30; i++) { + codes.add(discountCodeCommandService.create(request).code()); + } + + // 30번 중 최소 10개 이상 고유한 코드가 생성되어야 함 (완전 랜덤성 확인) + assertThat(codes.size()).isGreaterThan(10); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java new file mode 100644 index 00000000..03071914 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java @@ -0,0 +1,257 @@ +package OneQ.OnSurvey.domain.question.service; + +import OneQ.OnSurvey.domain.question.entity.question.*; +import OneQ.OnSurvey.domain.question.model.QuestionType; +import OneQ.OnSurvey.domain.question.model.dto.GridOptionDto; +import OneQ.OnSurvey.domain.question.model.dto.OptionDto; +import OneQ.OnSurvey.domain.question.model.dto.QuestionUpsertDto; +import OneQ.OnSurvey.domain.question.model.dto.type.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class QuestionConverterTest { + + // ── toQuestionDto ───────────────────────────────────────────────────────── + + @Test + @DisplayName("Choice 엔티티 → ChoiceDto 변환") + void toQuestionDto_choice() { + Choice choice = Choice.of( + 1L, 1, "Q1", "설명", true, 1, + 3, false, true, false, + QuestionType.CHOICE, null + ); + + DefaultQuestionDto dto = QuestionConverter.toQuestionDto(choice); + + assertThat(dto).isInstanceOf(ChoiceDto.class); + ChoiceDto choiceDto = (ChoiceDto) dto; + assertThat(choiceDto.getMaxChoice()).isEqualTo(3); + assertThat(choiceDto.getHasCustomInput()).isTrue(); + assertThat(choiceDto.getHasNoneOption()).isFalse(); + assertThat(choiceDto.getIsSectionDecidable()).isFalse(); + assertThat(choiceDto.getQuestionType()).isEqualTo(QuestionType.Values.CHOICE); + assertThat(choiceDto.getTitle()).isEqualTo("Q1"); + } + + @Test + @DisplayName("Rating 엔티티 → RatingDto 변환") + void toQuestionDto_rating() { + Rating rating = Rating.of( + 1L, 2, "평가", "설명", false, 1, + "매우 좋음", "매우 나쁨", 5, QuestionType.RATING, null + ); + + DefaultQuestionDto dto = QuestionConverter.toQuestionDto(rating); + + assertThat(dto).isInstanceOf(RatingDto.class); + RatingDto ratingDto = (RatingDto) dto; + assertThat(ratingDto.getRate()).isEqualTo(5); + assertThat(ratingDto.getMaxValue()).isEqualTo("매우 좋음"); + assertThat(ratingDto.getMinValue()).isEqualTo("매우 나쁨"); + assertThat(ratingDto.getQuestionType()).isEqualTo(QuestionType.Values.RATING); + } + + @Test + @DisplayName("DateAnswer 엔티티 → DateDto 변환") + void toQuestionDto_date() { + LocalDateTime now = LocalDateTime.of(2024, 1, 15, 12, 0); + DateAnswer dateAnswer = DateAnswer.of( + 1L, 3, "날짜 질문", null, false, 1, + now, QuestionType.DATE, null + ); + + DefaultQuestionDto dto = QuestionConverter.toQuestionDto(dateAnswer); + + assertThat(dto).isInstanceOf(DateDto.class); + DateDto dateDto = (DateDto) dto; + assertThat(dateDto.getDate()).isEqualTo(now); + assertThat(dateDto.getQuestionType()).isEqualTo(QuestionType.Values.DATE); + } + + @Test + @DisplayName("Grid 엔티티 → GridDto 변환") + void toQuestionDto_grid() { + Grid grid = Grid.of( + 1L, 4, "그리드 질문", null, true, 1, + QuestionType.GRID, null, true, false, true + ); + + DefaultQuestionDto dto = QuestionConverter.toQuestionDto(grid); + + assertThat(dto).isInstanceOf(GridDto.class); + GridDto gridDto = (GridDto) dto; + assertThat(gridDto.getIsCheckbox()).isTrue(); + assertThat(gridDto.getIsChoiceMixed()).isFalse(); + assertThat(gridDto.getIsChoiceDistinct()).isTrue(); + assertThat(gridDto.getQuestionType()).isEqualTo(QuestionType.Values.GRID); + } + + @Test + @DisplayName("Time 엔티티 → TimeDto 변환") + void toQuestionDto_time() { + Time time = Time.of( + 1L, 5, "시간 질문", null, false, 1, + QuestionType.TIME, null, true + ); + + DefaultQuestionDto dto = QuestionConverter.toQuestionDto(time); + + assertThat(dto).isInstanceOf(TimeDto.class); + TimeDto timeDto = (TimeDto) dto; + assertThat(timeDto.getIsInterval()).isTrue(); + assertThat(timeDto.getQuestionType()).isEqualTo(QuestionType.Values.TIME); + } + + @Test + @DisplayName("기본 질문 엔티티 → DefaultQuestionDto 변환") + void toQuestionDto_default() { + ShortAnswer shortAnswer = ShortAnswer.of( + 1L, 6, "단답형", null, false, 1, QuestionType.SHORT, null + ); + + DefaultQuestionDto dto = QuestionConverter.toQuestionDto(shortAnswer); + + assertThat(dto.getClass()).isEqualTo(DefaultQuestionDto.class); + assertThat(dto.getQuestionType()).isEqualTo(QuestionType.Values.SHORT); + assertThat(dto.getTitle()).isEqualTo("단답형"); + } + + // ── toQuestionUpsertDto ─────────────────────────────────────────────────── + + @Test + @DisplayName("ChoiceDto 리스트 → QuestionUpsertDto 변환 (기본 필드)") + void toQuestionUpsertDto_withChoiceDto() { + OptionDto option = OptionDto.builder() + .optionId(10L).content("선택지1").nextSection(null).imageUrl(null).build(); + + ChoiceDto choiceDto = ChoiceDto.builder() + .questionId(1L) + .questionType(QuestionType.Values.CHOICE) + .title("객관식 질문") + .description("설명") + .isRequired(true) + .questionOrder(1) + .section(1) + .maxChoice(2) + .hasNoneOption(false) + .hasCustomInput(true) + .isSectionDecidable(false) + .options(List.of(option)) + .build(); + + QuestionUpsertDto result = QuestionConverter.toQuestionUpsertDto(99L, List.of(choiceDto)); + + assertThat(result.getSurveyId()).isEqualTo(99L); + assertThat(result.getUpsertInfoList()).hasSize(1); + + QuestionUpsertDto.UpsertInfo info = result.getUpsertInfoList().get(0); + assertThat(info.getQuestionId()).isEqualTo(1L); + assertThat(info.getQuestionType()).isEqualTo(QuestionType.CHOICE); + assertThat(info.getMaxChoice()).isEqualTo(2); + assertThat(info.getHasCustomInput()).isTrue(); + assertThat(info.getOptions()).hasSize(1); + assertThat(info.getOptions().get(0).getContent()).isEqualTo("선택지1"); + } + + @Test + @DisplayName("RatingDto → UpsertInfo 변환") + void toQuestionUpsertDto_withRatingDto() { + RatingDto ratingDto = RatingDto.builder() + .questionId(2L) + .questionType(QuestionType.Values.RATING) + .title("평가 질문") + .isRequired(false) + .questionOrder(2) + .section(1) + .minValue("나쁨") + .maxValue("좋음") + .rate(10) + .build(); + + QuestionUpsertDto result = QuestionConverter.toQuestionUpsertDto(1L, List.of(ratingDto)); + + QuestionUpsertDto.UpsertInfo info = result.getUpsertInfoList().get(0); + assertThat(info.getQuestionType()).isEqualTo(QuestionType.RATING); + assertThat(info.getRate()).isEqualTo(10); + assertThat(info.getMinValue()).isEqualTo("나쁨"); + assertThat(info.getMaxValue()).isEqualTo("좋음"); + } + + @Test + @DisplayName("GridDto → UpsertInfo 변환") + void toQuestionUpsertDto_withGridDto() { + GridOptionDto gridOption = GridOptionDto.builder() + .gridOptionId(1L).isRow(true).content("행1").order(1).build(); + + GridDto gridDto = GridDto.builder() + .questionId(3L) + .questionType(QuestionType.Values.GRID) + .title("그리드 질문") + .isRequired(true) + .questionOrder(3) + .section(2) + .isCheckbox(true) + .isChoiceMixed(null) + .isChoiceDistinct(null) + .gridOptions(List.of(gridOption)) + .build(); + + QuestionUpsertDto result = QuestionConverter.toQuestionUpsertDto(1L, List.of(gridDto)); + + QuestionUpsertDto.UpsertInfo info = result.getUpsertInfoList().get(0); + assertThat(info.getIsCheckbox()).isTrue(); + assertThat(info.getIsChoiceMixed()).isFalse(); + assertThat(info.getIsChoiceDistinct()).isFalse(); + assertThat(info.getGridOptions()).hasSize(1); + } + + @Test + @DisplayName("TimeDto → UpsertInfo 변환 (isInterval null이면 false)") + void toQuestionUpsertDto_withTimeDto_nullIsInterval() { + TimeDto timeDto = TimeDto.builder() + .questionId(4L) + .questionType(QuestionType.Values.TIME) + .title("시간 질문") + .isRequired(false) + .questionOrder(4) + .section(1) + .isInterval(null) + .build(); + + QuestionUpsertDto result = QuestionConverter.toQuestionUpsertDto(1L, List.of(timeDto)); + + QuestionUpsertDto.UpsertInfo info = result.getUpsertInfoList().get(0); + assertThat(info.getIsInterval()).isFalse(); + } + + @Test + @DisplayName("section이 null이면 기본값 1로 설정") + void toQuestionUpsertDto_sectionNullDefaultsToOne() { + DefaultQuestionDto dto = DefaultQuestionDto.builder() + .questionId(5L) + .questionType(QuestionType.Values.SHORT) + .title("단답형") + .isRequired(false) + .questionOrder(1) + .section(null) + .build(); + + QuestionUpsertDto result = QuestionConverter.toQuestionUpsertDto(1L, List.of(dto)); + + assertThat(result.getUpsertInfoList().get(0).getSection()).isEqualTo(1); + } + + @Test + @DisplayName("빈 리스트 입력 시 upsertInfoList도 빈 리스트") + void toQuestionUpsertDto_emptyList() { + QuestionUpsertDto result = QuestionConverter.toQuestionUpsertDto(1L, List.of()); + + assertThat(result.getUpsertInfoList()).isEmpty(); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java new file mode 100644 index 00000000..e85d7f72 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java @@ -0,0 +1,331 @@ +package OneQ.OnSurvey.domain.survey.service.export; + +import OneQ.OnSurvey.domain.survey.SurveyErrorCode; +import OneQ.OnSurvey.domain.survey.entity.SurveyInfo; +import OneQ.OnSurvey.domain.survey.model.AgeRange; +import OneQ.OnSurvey.domain.survey.model.Gender; +import OneQ.OnSurvey.domain.survey.model.Residence; +import OneQ.OnSurvey.domain.survey.model.export.SurveyAnswerProjection; +import OneQ.OnSurvey.domain.survey.model.export.SurveyExportFile; +import OneQ.OnSurvey.domain.survey.model.export.SurveyMemberProjection; +import OneQ.OnSurvey.domain.survey.model.export.SurveyQuestionHeader; +import OneQ.OnSurvey.domain.survey.repository.export.SurveyExportRepository; +import OneQ.OnSurvey.domain.survey.repository.surveyInfo.SurveyInfoRepository; +import OneQ.OnSurvey.global.common.exception.CustomException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class SurveyExportServiceTest { + + @Mock + private SurveyExportRepository surveyExportRepository; + + @Mock + private SurveyInfoRepository surveyInfoRepository; + + @InjectMocks + private SurveyExportService surveyExportService; + + private static final Long SURVEY_ID = 1L; + private static final Long MEMBER_ID = 10L; + + @BeforeEach + void setUp() { + } + + private SurveyInfo buildSurveyInfo(Gender gender, Set ages) { + return SurveyInfo.builder() + .surveyId(SURVEY_ID) + .dueCount(100) + .completedCount(0) + .gender(gender) + .ages(ages) + .residences(Set.of(Residence.ALL)) + .genderPrice(0) + .agePrice(0) + .residencePrice(0) + .dueCountPrice(0) + .promotionAmount(0) + .build(); + } + + // ── exportCsv 권한 체크 ──────────────────────────────────────────────────── + + @Test + @DisplayName("소유자가 아니면 SURVEY_FORBIDDEN 예외 발생") + void exportCsv_notOwner_throwsForbidden() { + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(false); + + assertThatThrownBy(() -> surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.SURVEY_FORBIDDEN)); + } + + @Test + @DisplayName("SurveyInfo가 없으면 SURVEY_INFO_NOT_FOUND 예외 발생") + void exportCsv_surveyInfoNotFound_throwsException() { + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.empty()); + + assertThatThrownBy(() -> surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()) + .isEqualTo(SurveyErrorCode.SURVEY_INFO_NOT_FOUND)); + } + + // ── CSV 내용 검증 ───────────────────────────────────────────────────────── + + @Test + @DisplayName("gender=ALL이면 gender 컬럼 미포함") + void exportCsv_genderAll_noGenderColumn() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("테스트설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + assertThat(csv).doesNotContain("gender"); + } + + @Test + @DisplayName("gender=MALE이면 gender 컬럼 포함") + void exportCsv_specificGender_hasGenderColumn() { + SurveyInfo info = buildSurveyInfo(Gender.MALE, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("테스트설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + assertThat(csv).contains("gender"); + } + + @Test + @DisplayName("ages=ALL이면 age 컬럼 미포함") + void exportCsv_ageAll_noAgeColumn() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + assertThat(csv).doesNotContain("age"); + } + + @Test + @DisplayName("특정 연령대 설정 시 age 컬럼 포함") + void exportCsv_specificAge_hasAgeColumn() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.TWENTY)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + assertThat(csv).contains("age"); + } + + @Test + @DisplayName("질문 헤더와 응답자 데이터가 올바르게 CSV에 포함됨") + void exportCsv_withQuestionsAndMembers_csvContainsData() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + List headers = List.of( + new SurveyQuestionHeader(100L, 0, "좋아하는 음식", 0, null) + ); + List members = List.of( + new SurveyMemberProjection(MEMBER_ID, "19900101", "MALE", "서울") + ); + List answers = List.of( + new SurveyAnswerProjection(MEMBER_ID, 100L, "피자", 0) + ); + + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(headers); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(members); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(answers); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("음식설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + assertThat(csv).contains("Q1. 좋아하는 음식"); + assertThat(csv).contains("서울"); + assertThat(csv).contains("피자"); + } + + @Test + @DisplayName("CSV 파일에 BOM(UTF-8)이 포함됨") + void exportCsv_hasBom() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + assertThat(file.bytes()[0]).isEqualTo((byte) 0xEF); + assertThat(file.bytes()[1]).isEqualTo((byte) 0xBB); + assertThat(file.bytes()[2]).isEqualTo((byte) 0xBF); + } + + @Test + @DisplayName("파일명에 날짜가 포함되고 확장자가 .csv") + void exportCsv_filenameHasDateAndCsvExtension() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("음식 설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + assertThat(file.filename()).endsWith(".csv"); + assertThat(file.filename()).startsWith("음식 설문_"); + assertThat(file.contentType()).isEqualTo("text/csv; charset=UTF-8"); + } + + @Test + @DisplayName("파일명에 특수문자가 있으면 공백으로 치환") + void exportCsv_titleWithSpecialChars_sanitized() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("설문/테스트:조사*파일"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + assertThat(file.filename()).doesNotContain("/", ":", "*"); + } + + @Test + @DisplayName("surveyTitle이 null이면 파일명 기본값 'survey'") + void exportCsv_nullTitle_defaultFilename() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn(null); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + assertThat(file.filename()).startsWith("survey_"); + } + + @Test + @DisplayName("콤마 포함 응답은 큰따옴표로 감싸서 CSV 이스케이프") + void exportCsv_answerWithComma_escapedWithQuotes() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + List headers = List.of( + new SurveyQuestionHeader(100L, 0, "음식", 0, null) + ); + List members = List.of( + new SurveyMemberProjection(MEMBER_ID, null, null, "서울") + ); + List answers = List.of( + new SurveyAnswerProjection(MEMBER_ID, 100L, "피자,파스타", 0) + ); + + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(headers); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(members); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(answers); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + assertThat(csv).contains("\"피자,파스타\""); + } + + @Test + @DisplayName("exportCsvForAdmin은 소유자 체크 없이 CSV 생성") + void exportCsvForAdmin_noOwnerCheck() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.ALL)); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("관리자용설문"); + + SurveyExportFile file = surveyExportService.exportCsvForAdmin(SURVEY_ID); + + assertThat(file).isNotNull(); + assertThat(file.filename()).contains("관리자용설문"); + } + + @Test + @DisplayName("응답자의 생년월일로 나이 계산") + void exportCsv_withBirthday_ageCalculated() { + SurveyInfo info = buildSurveyInfo(Gender.ALL, Set.of(AgeRange.TWENTY)); + given(surveyExportRepository.existsOwnedSurvey(SURVEY_ID, MEMBER_ID)).willReturn(true); + given(surveyInfoRepository.findBySurveyId(SURVEY_ID)).willReturn(Optional.of(info)); + given(surveyExportRepository.findQuestionHeaders(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findMembersWhoAnswered(SURVEY_ID)).willReturn( + List.of(new SurveyMemberProjection(MEMBER_ID, "2000-01-01", null, "서울")) + ); + given(surveyExportRepository.findAnswers(SURVEY_ID)).willReturn(List.of()); + given(surveyExportRepository.findSurveyTitle(SURVEY_ID)).willReturn("설문"); + + SurveyExportFile file = surveyExportService.exportCsv(SURVEY_ID, MEMBER_ID); + + String csv = removeBom(file.bytes()); + // 2000년생이면 2026 - 2000 + 1 = 27 + assertThat(csv).contains("27"); + } + + private String removeBom(byte[] bytes) { + if (bytes.length >= 3 + && bytes[0] == (byte) 0xEF + && bytes[1] == (byte) 0xBB + && bytes[2] == (byte) 0xBF) { + return new String(bytes, 3, bytes.length - 3, StandardCharsets.UTF_8); + } + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/refund/DefaultSurveyRefundPolicyTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/refund/DefaultSurveyRefundPolicyTest.java new file mode 100644 index 00000000..115c72f5 --- /dev/null +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/refund/DefaultSurveyRefundPolicyTest.java @@ -0,0 +1,156 @@ +package OneQ.OnSurvey.domain.survey.service.refund; + +import OneQ.OnSurvey.domain.survey.entity.Survey; +import OneQ.OnSurvey.domain.survey.entity.SurveyInfo; +import OneQ.OnSurvey.domain.survey.model.Gender; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class DefaultSurveyRefundPolicyTest { + + private DefaultSurveyRefundPolicy policy; + + @BeforeEach + void setUp() { + policy = new DefaultSurveyRefundPolicy(); + ReflectionTestUtils.setField(policy, "rewardPerResponse", 100); + } + + private Survey surveyWithCoin(int totalCoin) { + return Survey.builder() + .memberId(1L) + .title("테스트 설문") + .description("설명") + .totalCoin(totalCoin) + .build(); + } + + private SurveyInfo surveyInfo(int dueCount, int completedCount) { + return SurveyInfo.builder() + .surveyId(1L) + .dueCount(dueCount) + .completedCount(completedCount) + .gender(Gender.ALL) + .ages(Set.of()) + .residences(Set.of()) + .genderPrice(0) + .agePrice(0) + .residencePrice(0) + .dueCountPrice(0) + .promotionAmount(0) + .build(); + } + + @Test + @DisplayName("목표 응답 수가 0 이하이면 환불 금액은 0") + void calculateRefundAmount_whenTargetCountZero_returnsZero() { + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(0, 0); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isZero(); + } + + @Test + @DisplayName("목표 응답 수가 음수이면 환불 금액은 0") + void calculateRefundAmount_whenTargetCountNegative_returnsZero() { + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(-1, 0); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isZero(); + } + + @Test + @DisplayName("완료 수가 목표 수 이상이면 환불 금액은 0") + void calculateRefundAmount_whenCompletedReachedTarget_returnsZero() { + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(10, 10); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isZero(); + } + + @Test + @DisplayName("완료 수가 목표 수를 초과해도 환불 금액은 0") + void calculateRefundAmount_whenCompletedExceedsTarget_returnsZero() { + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(10, 15); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isZero(); + } + + @Test + @DisplayName("응답이 0건일 때 전액 환불") + void calculateRefundAmount_whenNoCompletions_returnsFullRefund() { + // totalCoin=1000, rewardPerResponse=100, dueCount=10, completedCount=0 + // paidReward=0, refundBase=1000, lackRatio=1.0 → refund=1000 + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(10, 0); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isEqualTo(1000); + } + + @Test + @DisplayName("절반 응답 완료 시 남은 비율만큼 환불") + void calculateRefundAmount_whenHalfCompleted_returnsHalfRefund() { + // totalCoin=1000, rewardPerResponse=100, dueCount=10, completedCount=5 + // paidReward=500, refundBase=500, lackRatio=5/10=0.5 → refund=250 + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(10, 5); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isEqualTo(250); + } + + @Test + @DisplayName("totalCoin이 paidReward 이하이면 refundBase=0 → 환불 금액 0") + void calculateRefundAmount_whenTotalCoinBelowPaidReward_returnsZero() { + // totalCoin=300, rewardPerResponse=100, completedCount=5 → paidReward=500 > totalCoin + // refundBase=0 → refund=0 + Survey survey = surveyWithCoin(300); + SurveyInfo info = surveyInfo(10, 5); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isZero(); + } + + @Test + @DisplayName("소수점 이하는 내림 처리") + void calculateRefundAmount_floorsFractionalAmount() { + // totalCoin=1000, rewardPerResponse=100, dueCount=3, completedCount=1 + // paidReward=100, refundBase=900, lackRatio=2/3≈0.6667 → floor(600)=600 + Survey survey = surveyWithCoin(1000); + SurveyInfo info = surveyInfo(3, 1); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isEqualTo(600); + } + + @Test + @DisplayName("목표 1건 중 0건 완료 → 전액 환불") + void calculateRefundAmount_singleTargetNoCompletion_fullRefund() { + Survey survey = surveyWithCoin(500); + SurveyInfo info = surveyInfo(1, 0); + + int result = policy.calculateRefundAmount(survey, info); + + assertThat(result).isEqualTo(500); + } +} From 86b40c4fd5e2bd860ac320cc6a70a3c5ed425aa7 Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 15:06:40 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=84=B9=EC=85=98=20=EA=B5=AC=EB=B6=84=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/question/service/QuestionConverterTest.java | 4 ---- .../domain/survey/service/export/SurveyExportServiceTest.java | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java index 03071914..067a92c0 100644 --- a/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/question/service/QuestionConverterTest.java @@ -16,8 +16,6 @@ class QuestionConverterTest { - // ── toQuestionDto ───────────────────────────────────────────────────────── - @Test @DisplayName("Choice 엔티티 → ChoiceDto 변환") void toQuestionDto_choice() { @@ -122,8 +120,6 @@ void toQuestionDto_default() { assertThat(dto.getTitle()).isEqualTo("단답형"); } - // ── toQuestionUpsertDto ─────────────────────────────────────────────────── - @Test @DisplayName("ChoiceDto 리스트 → QuestionUpsertDto 변환 (기본 필드)") void toQuestionUpsertDto_withChoiceDto() { diff --git a/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java index e85d7f72..b45129f4 100644 --- a/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/survey/service/export/SurveyExportServiceTest.java @@ -64,8 +64,6 @@ private SurveyInfo buildSurveyInfo(Gender gender, Set ages) { .build(); } - // ── exportCsv 권한 체크 ──────────────────────────────────────────────────── - @Test @DisplayName("소유자가 아니면 SURVEY_FORBIDDEN 예외 발생") void exportCsv_notOwner_throwsForbidden() { @@ -89,8 +87,6 @@ void exportCsv_surveyInfoNotFound_throwsException() { .isEqualTo(SurveyErrorCode.SURVEY_INFO_NOT_FOUND)); } - // ── CSV 내용 검증 ───────────────────────────────────────────────────────── - @Test @DisplayName("gender=ALL이면 gender 컬럼 미포함") void exportCsv_genderAll_noGenderColumn() { From 4a18c9321f52f4f76b57b14aaaeb0bb9bc0f040c Mon Sep 17 00:00:00 2001 From: jaekwan Date: Sun, 10 May 2026 16:27:56 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20@RepeatedTest=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20=ED=99=95=EB=A5=A0=EC=A0=81=20=EB=8B=A4?= =?UTF-8?q?=EC=96=91=EC=84=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DiscountCodeCommandServiceTest.java | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java index 975e1d41..a67b2a9e 100644 --- a/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java +++ b/src/test/java/OneQ/OnSurvey/domain/discount/service/DiscountCodeCommandServiceTest.java @@ -5,7 +5,6 @@ import OneQ.OnSurvey.domain.discount.model.response.DiscountCodeResponse; import OneQ.OnSurvey.domain.discount.repository.DiscountCodeRepository; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -14,8 +13,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDate; -import java.util.HashSet; -import java.util.Set; import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; @@ -92,37 +89,4 @@ void create_savedEntity_hasCorrectOrgName() { assertThat(CODE_PATTERN.matcher(saved.getCode()).matches()).isTrue(); } - @RepeatedTest(20) - @DisplayName("반복 생성 시 매번 유효한 6자리 코드 생성") - void create_repeatedCalls_alwaysValidCode() { - CreateDiscountCodeRequest request = new CreateDiscountCodeRequest( - "RepeatedOrg", LocalDate.of(2027, 12, 31) - ); - given(discountCodeRepository.existsByCode(anyString())).willReturn(false); - given(discountCodeRepository.save(any(DiscountCode.class))) - .willAnswer(inv -> inv.getArgument(0)); - - DiscountCodeResponse response = discountCodeCommandService.create(request); - - assertThat(CODE_PATTERN.matcher(response.code()).matches()).isTrue(); - } - - @Test - @DisplayName("여러 번 생성 시 코드가 무작위로 다양하게 생성됨") - void create_multipleInvocations_producesDiverseCodes() { - CreateDiscountCodeRequest request = new CreateDiscountCodeRequest( - "Org", LocalDate.of(2027, 1, 1) - ); - given(discountCodeRepository.existsByCode(anyString())).willReturn(false); - given(discountCodeRepository.save(any(DiscountCode.class))) - .willAnswer(inv -> inv.getArgument(0)); - - Set codes = new HashSet<>(); - for (int i = 0; i < 30; i++) { - codes.add(discountCodeCommandService.create(request).code()); - } - - // 30번 중 최소 10개 이상 고유한 코드가 생성되어야 함 (완전 랜덤성 확인) - assertThat(codes.size()).isGreaterThan(10); - } }