테스트 객체 자동 생성 - depromeet/Took-BE GitHub Wiki
- Faker
- easy-random
package com.evenly.took.common.fixture;
import com.github.javafaker.Faker;
import org.jeasy.random.EasyRandom;
import org.jeasy.random.EasyRandomParameters;
import org.jeasy.random.randomizers.range.IntegerRangeRandomizer;
import org.jeasy.random.randomizers.range.LongRangeRandomizer;
import org.jeasy.random.randomizers.time.LocalDateRandomizer;
import org.jeasy.random.randomizers.time.LocalDateTimeRandomizer;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class EntityFixtureGenerator {
private final Faker faker;
private EasyRandom easyRandom;
private EasyRandomParameters parameters;
/**
* 기본 생성자 - 한국어 데이터와 기본 설정으로 초기화
*/
public EntityFixtureGenerator() {
this(new Locale("ko"));
}
/**
* 특정 언어 설정으로 초기화
*/
public EntityFixtureGenerator(Locale locale) {
this.faker = new Faker(locale);
initializeParameters();
this.easyRandom = new EasyRandom(parameters);
}
/**
* 기본 파라미터 설정 초기화
*/
private void initializeParameters() {
Random random = new Random();
this.parameters = new EasyRandomParameters()
// 일관된 테스트 결과를 위한 시드 설정 (필요시 변경 가능)
.seed(42L)
// 기본 타입에 대한 랜덤화 설정
.randomize(Long.class, new LongRangeRandomizer(1L, 1000L))
.randomize(Integer.class, new IntegerRangeRandomizer(1, 100))
.randomize(Boolean.class, () -> random.nextBoolean())
// 날짜 관련 필드 설정
.randomize(LocalDate.class, new LocalDateRandomizer(
LocalDate.now().minusYears(1),
LocalDate.now())
)
.randomize(LocalDateTime.class, new LocalDateTimeRandomizer(
LocalDateTime.now().minusMonths(6),
LocalDateTime.now())
)
// 문자열 필드의 기본 랜덤화 설정 (아래 케이스 중 하나로 랜덤하게 삽입)
.randomize(String.class, () -> {
switch (random.nextInt(5)) {
case 0: return faker.name().fullName();
case 1: return faker.company().name();
case 2: return faker.internet().url();
case 3: return faker.lorem().sentence(3, 5);
default: return faker.address().cityName();
}
})
// 특정 필드명에 대한 커스텀 랜덤화 설정
.randomize(field -> field.getName().equals("nickname") || field.getName().equals("username"),
() -> faker.name().username())
.randomize(field -> field.getName().equals("email"),
() -> faker.internet().emailAddress())
.randomize(field -> field.getName().equals("name"),
() -> faker.name().fullName())
.randomize(field -> field.getName().equals("organization") || field.getName().equals("company"),
() -> faker.company().name())
.randomize(field -> field.getName().equals("imagePath") || field.getName().equals("imageUrl"),
() -> "images/" + faker.file().fileName() + ".jpg")
.randomize(field -> field.getName().equals("description") || field.getName().equals("summary"),
() -> faker.lorem().paragraph())
.randomize(field -> field.getName().equals("phone") || field.getName().equals("phoneNumber"),
() -> faker.phoneNumber().phoneNumber())
.randomize(field -> field.getName().equals("address"),
() -> faker.address().fullAddress())
// 컬렉션 필드의 크기 설정
.collectionSizeRange(1, 5)
// 일반적으로 제외할 필드 설정
.excludeField(field -> field.getName().equals("id"))
.excludeField(field -> field.getName().equals("createdAt"))
.excludeField(field -> field.getName().equals("updatedAt"))
.excludeField(field -> field.getName().equals("deletedAt"));
}
/**
* 단일 엔티티 객체 생성
*/
public <T> T createRandom(Class<T> clazz) {
return easyRandom.nextObject(clazz);
}
/**
* ID 값까지 함께 설정한 엔티티 객체 생성
*/
public <T> T createRandomWithId(Class<T> clazz, Long id) {
T entity = easyRandom.nextObject(clazz);
setFieldValue(entity, "id", id);
return entity;
}
/**
* 여러 엔티티 객체 리스트 생성
*/
public <T> List<T> createRandomList(Class<T> clazz, int size) {
return easyRandom.objects(clazz, size)
.collect(Collectors.toList());
}
/**
* ID를 1부터 순차적으로 할당하여 여러 엔티티 객체 생성
*/
public <T> List<T> createRandomListWithSequentialIds(Class<T> clazz, int size) {
List<T> entities = createRandomList(clazz, size);
for (int i = 0; i < entities.size(); i++) {
setFieldValue(entities.get(i), "id", (long)(i + 1));
}
return entities;
}
/**
* 특정 필드를 제외하고 엔티티 생성
*/
public <T> T createRandomExcluding(Class<T> clazz, String... excludeFields) {
// 기존 설정을 복제하여 새로운 설정 생성
EasyRandomParameters newParams = cloneParameters();
// 추가 제외 필드 설정
for (String fieldName : excludeFields) {
newParams.excludeField(field -> field.getName().equals(fieldName));
}
// 임시 EasyRandom 인스턴스로 객체 생성
EasyRandom tempRandom = new EasyRandom(newParams);
return tempRandom.nextObject(clazz);
}
/**
* 특정 조건을 만족하는 필드를 제외하고 엔티티 생성
*/
public <T> T createRandomExcludingWhen(Class<T> clazz, Predicate<Field> excludeCondition) {
EasyRandomParameters newParams = cloneParameters();
newParams.excludeField(excludeCondition);
EasyRandom tempRandom = new EasyRandom(newParams);
return tempRandom.nextObject(clazz);
}
/**
* 특정 필드에 특정 값을 가진 엔티티 생성
*/
public <T, V> T createRandomWith(Class<T> clazz, String fieldName, V value) {
T entity = createRandom(clazz);
setFieldValue(entity, fieldName, value);
return entity;
}
/**
* 현재 설정을 복제하는 헬퍼 메서드
*/
private EasyRandomParameters cloneParameters() {
EasyRandomParameters newParams = new EasyRandomParameters()
.seed(parameters.getSeed())
.objectPoolSize(parameters.getObjectPoolSize())
.randomizationDepth(parameters.getRandomizationDepth())
.collectionSizeRange(
parameters.getCollectionSizeRange().getMin(),
parameters.getCollectionSizeRange().getMax()
);
// 나머지 설정 복사 (랜더마이저, 제외 필드 등)
// 실제 구현은 더 복잡할 수 있습니다
return newParams;
}
/**
* 리플렉션을 사용하여 필드 값 설정
*/
private <T, V> void setFieldValue(T entity, String fieldName, V value) {
try {
Field field = findField(entity.getClass(), fieldName);
if (field != null) {
field.setAccessible(true);
field.set(entity, value);
}
} catch (Exception e) {
throw new RuntimeException("필드 '" + fieldName + "' 값 설정 실패", e);
}
}
/**
* 상속 계층구조에서 필드 찾기
*/
private Field findField(Class<?> clazz, String fieldName) {
Class<?> currentClass = clazz;
while (currentClass != null) {
try {
return currentClass.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
currentClass = currentClass.getSuperclass();
}
}
return null;
}
/**
* 특정 타입에 대한 커스텀 랜더마이저 추가
*/
public <T> EntityFixtureGenerator withCustomRandomizer(Class<T> type, Supplier<T> randomizer) {
parameters.randomize(type, randomizer::get);
easyRandom = new EasyRandom(parameters);
return this;
}
/**
* 특정 필드에 대한 커스텀 랜더마이저 추가
*/
public EntityFixtureGenerator withCustomFieldRandomizer(String fieldName, Supplier<?> randomizer) {
parameters.randomize(field -> field.getName().equals(fieldName), randomizer::get);
easyRandom = new EasyRandom(parameters);
return this;
}
/**
* 특정 필드에 대한 커스텀 랜더마이저 추가 (조건부)
*/
public EntityFixtureGenerator withCustomFieldRandomizer(Predicate<Field> condition, Supplier<?> randomizer) {
parameters.randomize(condition, randomizer::get);
easyRandom = new EasyRandom(parameters);
return this;
}
/**
* 기존 제외 필드 목록에 새로운 필드 추가
*/
public EntityFixtureGenerator excludeField(String fieldName) {
parameters.excludeField(field -> field.getName().equals(fieldName));
easyRandom = new EasyRandom(parameters);
return this;
}
/**
* 기존 제외 필드 목록에 조건부 필드 추가
*/
public EntityFixtureGenerator excludeFieldWhen(Predicate<Field> condition) {
parameters.excludeField(condition);
easyRandom = new EasyRandom(parameters);
return this;
}
/**
* 컬렉션 크기 범위 설정
*/
public EntityFixtureGenerator withCollectionSizeRange(int min, int max) {
parameters.collectionSizeRange(min, max);
easyRandom = new EasyRandom(parameters);
return this;
}
/**
* 시드 값 설정
*/
public EntityFixtureGenerator withSeed(long seed) {
parameters.seed(seed);
easyRandom = new EasyRandom(parameters);
return this;
}
}
.excludeField()
메서드를 사용하여 특정 필드를 랜덤 데이터 생성에서 제외할 수 있습니다:
// 일반적으로 제외할 필드 설정
.excludeField(field -> field.getName().equals("id"))
.excludeField(field -> field.getName().equals("createdDate"))
.excludeField(field -> field.getName().equals("lastModifiedDate"))
이 코드는:
- JPA 엔티티에서 흔히 볼 수 있는
id
,createdAt
,updatedAt
등의 필드를 무작위 생성에서 제외합니다. - 이런 필드들은 보통 자동 생성되거나 테스트에서 명시적으로 설정해야 할 수 있어 제외하는 것이 좋습니다.
모든 String 타입 필드에 대한 기본 랜덤화 설정은 다음과 같이 작동합니다
.randomize(String.class, () -> {
switch (random.nextInt(5)) {
case 0: return faker.name().fullName(); // 이름 (예: "홍길동")
case 1: return faker.company().name(); // 회사명 (예: "삼성전자")
case 2: return faker.internet().url(); // URL (예: "http://example.org")
case 3: return faker.lorem().sentence(3, 5); // 3-5단어로 구성된 문장
default: return faker.address().cityName(); // 도시명 (예: "서울")
}
})
하지만 특정 필드명에 대해서는 더 적합한 데이터가 생성되도록 오버라이드합니다:
.randomize(field -> field.getName().equals("email"),
() -> faker.internet().emailAddress()) // 항상 이메일 형식 (예: "[email protected]")
.randomize(field -> field.getName().equals("description"),
() -> faker.lorem().paragraph()) // 항상 단락 텍스트
이 확장된 구현은 다음과 같은 유용한 기능을 제공합니다:
-
다양한 생성 방법:
// 기본 랜덤 생성 Card card = generator.createRandom(Card.class); // ID를 포함한 랜덤 생성 Card cardWithId = generator.createRandomWithId(Card.class, 42L); // 특정 필드 제외 Card cardWithoutSummary = generator.createRandomExcluding(Card.class, "summary", "news"); // 특정 값 포함 Card premiumCard = generator.createRandomWith(Card.class, "tier", "PREMIUM");
-
메서드 체이닝으로 설정 변경:
// 메서드 체이닝으로 설정 변경 후 생성 Card specialCard = generator .withSeed(123L) .withCollectionSizeRange(2, 4) .excludeField("createdAt") .withCustomFieldRandomizer("nickname", () -> "특별한닉네임") .createRandom(Card.class);
-
일괄 생성 기능:
// 10개의 카드 생성 List<Card> cards = generator.createRandomList(Card.class, 10); // ID가 1부터 순차적으로 할당된 10개의 카드 생성 List<Card> cardsWithIds = generator.createRandomListWithSequentialIds(Card.class, 10);
// 기본 생성자로 생성 (한국어 데이터)
EntityFixtureGenerator generator = new EntityFixtureGenerator();
// 단일 엔티티 생성
Card card = generator.createRandom(Card.class);
// 여러 엔티티 생성
List<Card> cards = generator.createRandomList(Card.class, 5);
// 특정 필드에 특정 값을 갖는 엔티티 생성
Card developerCard = generator.createRandomWith(Card.class, "career", Career.DEVELOPER);
// 특정 필드를 제외한 엔티티 생성
Card cardWithoutSnsLinks = generator.createRandomExcluding(Card.class, "sns");
// 특수 목적 제너레이터 구성
EntityFixtureGenerator specialGenerator = new EntityFixtureGenerator()
.withCustomFieldRandomizer("summary", () -> "모든 카드에 동일한 요약 정보")
.excludeField("news")
.withCollectionSizeRange(3, 3); // 항상 컬렉션 크기가 3인 경우
Card specialCard = specialGenerator.createRandom(Card.class);