Skip to content

Commit

Permalink
#30 - 가짜 데이터 생성기 및 전략 패턴 컨텍스트 구현
Browse files Browse the repository at this point in the history
설계 �구성은 #29 파일 출력기 컨텍스트의 패턴과 동일.
시간 관계상 구현체는 기본이 되는 문자열 생성기
하나만 강의 중에 작성함.
문자열 생성기가 의도에 맞는 문자열을 생성하는지 보는
다양한 테스트들을 작성.
  • Loading branch information
djkeh committed Aug 7, 2024
1 parent 66a9382 commit ef5f087
Show file tree
Hide file tree
Showing 5 changed files with 379 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package uno.fastcampus.testdata.service.generator;

import uno.fastcampus.testdata.domain.constant.MockDataType;

/**
* 가짜 데이터를 발생시키는 인터페이스
*
* @author Uno
*/
public interface MockDataGenerator {

/**
* 구현체가 처리하는 가짜 데이터 타입을 반환하는 메소드.
*
* @return 이 구현체가 다루는 가짜 데이터의 유형
*/
MockDataType getType();

/**
* 가짜 데이터를 생성하는 메소드.
*
* @param blankPercent 빈 값의 비율, 백분율(0 ~ 100). 0이면 빈 값이 없음, 100이면 모두 빈 값({@code null}).
* @param typeOptionJson 가짜 데이터 생성에 필요한 부가 옵션을 담은 JSON 문자열. 구현체에 따라 다르게 해석됨.
* @param forceValue 강제로 설정할 값. 이 값이 주어지면, 가짜 데이터를 생성하지 않고 무조건 이 값으로 반환함.
* @return 생성된 가짜 데이터 문자열
*/
String generate(Integer blankPercent, String typeOptionJson, String forceValue);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package uno.fastcampus.testdata.service.generator;

import org.springframework.stereotype.Service;
import uno.fastcampus.testdata.domain.constant.MockDataType;

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
public class MockDataGeneratorContext {

private final Map<MockDataType, MockDataGenerator> mockDataGeneratorMap;

public MockDataGeneratorContext(List<MockDataGenerator> mockDataGenerators) {
this.mockDataGeneratorMap = mockDataGenerators.stream()
.collect(Collectors.toMap(MockDataGenerator::getType, Function.identity()));
}

public String generate(MockDataType mockDataType, Integer blankPercent, String typeOptionJson, String forceValue) {
MockDataGenerator generator = mockDataGeneratorMap.get(mockDataType);

// TODO: 다양한 가짜 데이터 생성기가 도입되면, 이 기본값 강제 설정 코드를 삭제할 것.
if (generator == null) {
generator = mockDataGeneratorMap.get(MockDataType.STRING);
}

return generator.generate(blankPercent, typeOptionJson, forceValue);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package uno.fastcampus.testdata.service.generator;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import uno.fastcampus.testdata.domain.constant.MockDataType;
import uno.fastcampus.testdata.dto.MockDataDto;
import uno.fastcampus.testdata.repository.MockDataRepository;

import java.util.List;
import java.util.random.RandomGenerator;
import java.util.stream.Collectors;

@Slf4j
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Component
public class StringGenerator implements MockDataGenerator {

private final MockDataRepository mockDataRepository;
private final ObjectMapper mapper;

@Override
public MockDataType getType() {
return MockDataType.STRING;
}

@Override
public String generate(Integer blankPercent, String typeOptionJson, String forceValue) {
RandomGenerator randomGenerator = RandomGenerator.getDefault();
if (randomGenerator.nextInt(100) < blankPercent) {
return null;
}

if (forceValue != null && !forceValue.isBlank()) {
return forceValue;
}

Option option = new Option(1, 10); // 기본 옵션
try {
if (typeOptionJson != null && !typeOptionJson.isBlank()) {
option = mapper.readValue(typeOptionJson, Option.class);
}
} catch (JsonProcessingException e) {
log.warn("Json 옵션 정보를 읽어들이는데 실패하였습니다. 기본 옵션으로 동작합니다 - 입력 옵션: {}, 필요 옵션 예: {}", typeOptionJson, option);
}

if (option.minLength() < 1) {
throw new IllegalArgumentException("[가짜 데이터 생성 옵션 오류] 최소 길이가 1보다 작습니다 - option: " + typeOptionJson);
} else if (option.maxLength() > 10) {
throw new IllegalArgumentException("[가짜 데이터 생성 옵션 오류] 최대 길이가 10보다 큽니다 - option: " + typeOptionJson);
} else if (option.minLength() > option.maxLength()) {
throw new IllegalArgumentException("[가짜 데이터 생성 옵션 오류] 최소 길이가 최대 길이보다 큽니다 - option: " + typeOptionJson);
}

List<MockDataDto> mockDataDtos = mockDataRepository.findByMockDataType(getType())
.stream().map(MockDataDto::fromEntity).toList();
String body = mockDataDtos.stream()
.map(MockDataDto::mockDataValue)
.collect(Collectors.joining(""))
.replaceAll("[^가-힣]", "");



int difference = option.maxLength() - option.minLength();
int point = randomGenerator.nextInt(body.length() - option.maxLength);
int offset = (randomGenerator.nextInt(Math.max(1, difference))) + option.minLength();

return body.substring(point, point + offset);
}

public record Option(Integer minLength, Integer maxLength) {}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package uno.fastcampus.testdata.service.generator;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import uno.fastcampus.testdata.domain.constant.MockDataType;

import static org.assertj.core.api.Assertions.assertThat;

@ActiveProfiles("test")
@DisplayName("[IntegrationTest] 파일 출력기 컨텍스트 테스트")
@SpringBootTest
record MockDataGeneratorContextTest(@Autowired MockDataGeneratorContext sut) {

@DisplayName("문자열 가짜 데이터 타입이 주어지면, 랜덤 문자열을 반환한다.")
@Test
void givenStringType_whenGeneratingMockData_thenReturnsRandomString() {
// Given

// When
String result = sut.generate(MockDataType.STRING, 0, null, null);

// Then
System.out.println(result); // 관찰용
assertThat(result).containsPattern("^[가-힣]{1,10}$");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package uno.fastcampus.testdata.service.generator;

import com.fasterxml.jackson.databind.ObjectMapper;
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.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import uno.fastcampus.testdata.domain.MockData;
import uno.fastcampus.testdata.domain.constant.MockDataType;
import uno.fastcampus.testdata.repository.MockDataRepository;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;

@DisplayName("[Logic] 문자열 데이터 생성기 테스트")
@ExtendWith(MockitoExtension.class)
class StringGeneratorTest {

@InjectMocks private StringGenerator sut;

@Mock private MockDataRepository mockDataRepository;
@Spy private ObjectMapper mapper;

@DisplayName("이 테스트 문자열 타입의 가짜 데이터를 다룬다.")
@Test
void givenNothing_whenCheckingType_thenReturnsStringType() {
// Given

// When & Then
assertThat(sut.getType()).isEqualTo(MockDataType.STRING);
}

@DisplayName("옵션에 따라 정규식을 통과하는 랜덤 문자열을 생성한다.")
@RepeatedTest(10)
void givenParams_whenGenerating_thenReturnsRandomText() throws Exception {
// Given
MockDataType mockDataType = sut.getType();
StringGenerator.Option option = new StringGenerator.Option(1, 10);
String optionJson = mapper.writeValueAsString(option);
given(mockDataRepository.findByMockDataType(mockDataType)).willReturn(List.of(
MockData.of(mockDataType, "새침하게 흐린 품이 눈이 올 듯하더니 눈은 아니 오고 얼다가 만 비가 추적추적 내리었다."),
MockData.of(mockDataType, "이날이야말로 동소문 안에서 인력거꾼 노릇을 하는 김 첨지에게는 오래간만에도 닥친 운수 좋은 날이었다."),
MockData.of(mockDataType, "문안에(거기도 문밖은 아니지만) 들어간답시는 앞집 마나님을 전찻길까지 모셔다 드린 것을 비롯으로 행여나 손님이 있을까 하고 정류장에서 어정어정하며 내리는 사람 하나하나에게 거의 비는 듯한 눈결을 보내고 있다가 마침내 교원인 듯한 양복장이를 동광학교(東光學校)까지 태워다 주기로 되었다.")
));

// When
String result = sut.generate(0, optionJson, null);

// Then
System.out.println(result); // 관찰용
assertThat(result).containsPattern("^[가-힣]{1,10}$");
then(mapper).should().readValue(optionJson, StringGenerator.Option.class);
then(mockDataRepository).should().findByMockDataType(mockDataType);
}

@CsvSource(delimiter = '|', textBlock = """
{"minLength":1,"maxLength":1} | ^[가-힣]{1,1}$
{"minLength":1,"maxLength":2} | ^[가-힣]{1,2}$
{"minLength":1,"maxLength":3} | ^[가-힣]{1,3}$
{"minLength":1,"maxLength":10} | ^[가-힣]{1,10}$
{"minLength":5,"maxLength":10} | ^[가-힣]{5,10}$
{"minLength":6,"maxLength":10} | ^[가-힣]{6,10}$
{"minLength":7,"maxLength":10} | ^[가-힣]{7,10}$
{"minLength":9,"maxLength":10} | ^[가-힣]{9,10}$
""")
@DisplayName("옵션에 따라 정규식을 통과하는 랜덤 문자열을 생성한다.")
@ParameterizedTest(name = "{index}. {0} ===> 통과 정규식: {1}")
void givenParams_whenGenerating_thenReturnsRandomText(String optionJson, String expectedRegex) throws Exception {
// Given
MockDataType mockDataType = sut.getType();
given(mockDataRepository.findByMockDataType(mockDataType)).willReturn(List.of(
MockData.of(mockDataType, "새침하게 흐린 품이 눈이 올 듯하더니 눈은 아니 오고 얼다가 만 비가 추적추적 내리었다."),
MockData.of(mockDataType, "이날이야말로 동소문 안에서 인력거꾼 노릇을 하는 김 첨지에게는 오래간만에도 닥친 운수 좋은 날이었다."),
MockData.of(mockDataType, "문안에(거기도 문밖은 아니지만) 들어간답시는 앞집 마나님을 전찻길까지 모셔다 드린 것을 비롯으로 행여나 손님이 있을까 하고 정류장에서 어정어정하며 내리는 사람 하나하나에게 거의 비는 듯한 눈결을 보내고 있다가 마침내 교원인 듯한 양복장이를 동광학교(東光學校)까지 태워다 주기로 되었다.")
));

// When
String result = sut.generate(0, optionJson, null);

// Then
System.out.println(result); // 관찰용
assertThat(result).containsPattern("^[가-힣]{1,10}$");
then(mapper).should().readValue(optionJson, StringGenerator.Option.class);
then(mockDataRepository).should().findByMockDataType(mockDataType);
}

@DisplayName("잘못된 옵션 내용이 주어지면, 예외를 던진다.")
@CsvSource(delimiter = '|', textBlock = """
{"minLength":0,"maxLength":0}
{"minLength":0,"maxLength":1}
{"minLength":0,"maxLength":10}
{"minLength":0,"maxLength":11}
{"minLength":1,"maxLength":11}
{"minLength":2,"maxLength":1}
{"minLength":5,"maxLength":1}
{"minLength":10,"maxLength":9}
""")
@ParameterizedTest(name = "{index}. 잘못된 옵션 - {0}")
void givenWrongOption_whenGenerating_thenThrowsException(String optionJson) throws Exception {
// Given

// When
Throwable t = catchThrowable(() -> sut.generate(0, optionJson, null));

// Then
assertThat(t)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageStartingWith("[가짜 데이터 생성 옵션 오류]");
then(mapper).should().readValue(optionJson, StringGenerator.Option.class);
then(mockDataRepository).shouldHaveNoInteractions();
}

@DisplayName("옵션이 없으면, 기본 옵션으로 정규식을 통과하는 랜덤 문자열을 생성한다.")
@RepeatedTest(10)
void givenNoParams_whenGenerating_thenReturnsRandomText() {
// Given
MockDataType mockDataType = sut.getType();
given(mockDataRepository.findByMockDataType(mockDataType)).willReturn(List.of(
MockData.of(mockDataType, "새침하게 흐린 품이 눈이 올 듯하더니 눈은 아니 오고 얼다가 만 비가 추적추적 내리었다."),
MockData.of(mockDataType, "이날이야말로 동소문 안에서 인력거꾼 노릇을 하는 김 첨지에게는 오래간만에도 닥친 운수 좋은 날이었다."),
MockData.of(mockDataType, "문안에(거기도 문밖은 아니지만) 들어간답시는 앞집 마나님을 전찻길까지 모셔다 드린 것을 비롯으로 행여나 손님이 있을까 하고 정류장에서 어정어정하며 내리는 사람 하나하나에게 거의 비는 듯한 눈결을 보내고 있다가 마침내 교원인 듯한 양복장이를 동광학교(東光學校)까지 태워다 주기로 되었다.")
));

// When
String result = sut.generate(0, null, null);

// Then
System.out.println(result); // 관찰용
assertThat(result).containsPattern("^[가-힣]{1,10}$");
then(mapper).shouldHaveNoInteractions();
then(mockDataRepository).should().findByMockDataType(mockDataType);
}

@DisplayName("옵션 json 형식이 잘못되었으면, 기본 옵션으로 정규식을 통과하는 랜덤 문자열을 생성한다.")
@Test
void givenWrongOptionJson_whenGenerating_thenReturnsRandomText() throws Exception {
// Given
MockDataType mockDataType = sut.getType();
String wrongJson = "{wrong json format}";
given(mockDataRepository.findByMockDataType(mockDataType)).willReturn(List.of(
MockData.of(mockDataType, "새침하게 흐린 품이 눈이 올 듯하더니 눈은 아니 오고 얼다가 만 비가 추적추적 내리었다."),
MockData.of(mockDataType, "이날이야말로 동소문 안에서 인력거꾼 노릇을 하는 김 첨지에게는 오래간만에도 닥친 운수 좋은 날이었다."),
MockData.of(mockDataType, "문안에(거기도 문밖은 아니지만) 들어간답시는 앞집 마나님을 전찻길까지 모셔다 드린 것을 비롯으로 행여나 손님이 있을까 하고 정류장에서 어정어정하며 내리는 사람 하나하나에게 거의 비는 듯한 눈결을 보내고 있다가 마침내 교원인 듯한 양복장이를 동광학교(東光學校)까지 태워다 주기로 되었다.")
));

// When
String result = sut.generate(0, wrongJson, null);

// Then
System.out.println(result); // 관찰용
assertThat(result).containsPattern("^[가-힣]{1,10}$");
then(mapper).should().readValue(wrongJson, StringGenerator.Option.class);
then(mockDataRepository).should().findByMockDataType(mockDataType);
}

@DisplayName("blank 옵션이 100%면, null을 생성한다.")
@RepeatedTest(10)
void givenNoParams_whenGenerating_thenReturnsNull() {
// Given

// When
String result = sut.generate(100, null, null);

// Then
assertThat(result).isEqualTo(null);
then(mapper).shouldHaveNoInteractions();
then(mockDataRepository).shouldHaveNoInteractions();
}

@DisplayName("강제 값이 주어지면, 강제 값을 그대로 반환한다.")
@Test
void givenForcedValue_whenGenerating_thenReturnsForcedValue() {
// Given
String forceValue = "직접입력";

// When
String result = sut.generate(0, null, forceValue);

// Then
assertThat(result).isEqualTo(forceValue);
then(mapper).shouldHaveNoInteractions();
then(mockDataRepository).shouldHaveNoInteractions();
}

@DisplayName("강제 값이 주어지더라도, blank 옵션에 따라 null을 반환할 수 있다.")
@Test
void givenBlankOptionAndForcedValue_whenGenerating_thenReturnsNull() {
// Given
int blankPercent = 100;
String forceValue = "직접입력";

// When
String result = sut.generate(blankPercent, null, forceValue);

// Then
assertThat(result).isNull();
then(mapper).shouldHaveNoInteractions();
then(mockDataRepository).shouldHaveNoInteractions();
}

}

0 comments on commit ef5f087

Please sign in to comment.