테스트 객체 자동 생성 - 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;
    }
}

EntityFixtureGenerator 주요 기능 설명

1. exclude 기능

.excludeField() 메서드를 사용하여 특정 필드를 랜덤 데이터 생성에서 제외할 수 있습니다:

// 일반적으로 제외할 필드 설정
.excludeField(field -> field.getName().equals("id"))
.excludeField(field -> field.getName().equals("createdDate"))
.excludeField(field -> field.getName().equals("lastModifiedDate"))

이 코드는:

  1. JPA 엔티티에서 흔히 볼 수 있는 id, createdAt, updatedAt 등의 필드를 무작위 생성에서 제외합니다.
  2. 이런 필드들은 보통 자동 생성되거나 테스트에서 명시적으로 설정해야 할 수 있어 제외하는 것이 좋습니다.

2. String 타입 필드 랜덤화 상세 설명

모든 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())             // 항상 단락 텍스트

3. 향상된 기능

이 확장된 구현은 다음과 같은 유용한 기능을 제공합니다:

  1. 다양한 생성 방법:

    // 기본 랜덤 생성
    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");
  2. 메서드 체이닝으로 설정 변경:

    // 메서드 체이닝으로 설정 변경 후 생성
    Card specialCard = generator
        .withSeed(123L)
        .withCollectionSizeRange(2, 4)
        .excludeField("createdAt")
        .withCustomFieldRandomizer("nickname", () -> "특별한닉네임")
        .createRandom(Card.class);
  3. 일괄 생성 기능:

    // 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);
⚠️ **GitHub.com Fallback** ⚠️