TEST

테스트 코드 - 더미 데이터 쉽게 만들기 (Easy Random) 사용하기 (2)

류큐큐 2024. 3. 19. 10:55

전 포스트에서 EasyRandom의 내부 코드 로직을 살펴보았다.

그럼 이제 이걸 어떻게 사용할것이냐


몇가지 상황을 예시로 들어보자.

내가 주문 시스템을 개발해야한다고 가정했을때

총 세가지 시나리오가 있다고 가정해보자.

1. 유효하지 않는 상품을 주문하려할때
2. 유효한 제품으로 주문을 성공할때
3. 유효하지 않은 요청으로 주문을 시도할때

그러면 이 세가지 시나리오에 대한 주문 단위 테스트를 만들어야하는데

@Test
fun placeOrderWithoutProductShouldError() {
    val customer = Customer("Peter", emptyList())

    assertThrows<IllegalArgumentException> {  orderService.placeOrder(customer) }
    verify(orderRepository, never()).save(customer)
}

@Test
fun placeOrderWithOneProductShouldBeOk() {
    val product = Product("AB101", "Product 1", "The first product")
    val order = Order(product, 10)
    val customer = Customer("Peter", listOf(order))

    orderService.placeOrder(customer)
    verify(orderRepository).save(customer)
}

@Test
fun placeOrderWithHighQuantityShouldError() {
    val product = Product("AB101", "Product 1", "The first product")
    val order = Order(product, 10000)
    val customer = Customer("Peter", listOf(order))

    assertThrows<IllegalArgumentException> {  orderService.placeOrder(customer) }
    verify(orderRepository, never()).save(customer)
}

출처 : https://jworks.io/easy-testing-with-objectmothers-and-easyrandom


위의 게시글에서 나온거처럼 저 객체를 생성할때 약간의 차이만 있을뿐 큰 차이는 없는데 저렇게 중복적으로 객체를 생성하는것 자체가 관리포인트가 된다는점이 문제인것이다.


그래서 Object Mothers라는 패턴을 사용하는데 간략하게 테스트 데이터 팩토리다. 기본 아이디어는 테스트에 필요한 객체를 미리 정의된 상태로 쉽게 생성하고자 한다.


 여기서 핵심 포인트
   1. Object Mothers는 특정 상태의 객체를 생성하는데 초점을 맞추자.
   2. 실제 값에 의존하지 않는다.

   3. 상태 설정에 중점을 둬야한다.

   4. 특정 실제값이 중요하다면 테스트 코드 단에서 Object Mothers로 생성된 객체에 값을 설정하자. 



그런데 여기서 실제 값에 의존하지 않는다 라는말이 어색할수도 있다.

내가 회원 도메인의 엔티티를 검증한다고 했을때 회원의 이메일 필드가 있다고 가정해보자.

이지 랜덤으로 무작위 값을 생성한다고 했을때 이메일 형식에 맞춰서 값을 생성하도록 변경해야하는데

EasyRandomParameters()
    .randomize(
        field().named("email").ofType(String::class.java).inClass(Member::class.java),
        { faker.internet().emailAddress() }     )


뭐 위와같이 이지 랜덤을 커스터마이징 했다고 치면 이 이메일 형식을 지정하는것 자체 (faker.internet().emailAddress() ) 가 어느정도 실제 값에 의존하는것 처럼 보일 수 있는데

여기서 중요한건 테스트의 목적과 컨텍스트를 이해해야한다.

실제 값에 의존하는 테스트는 특정 값을 기대하며 이 값이 변경되면 테스트가 실패할 수 있다.


반면  Easy Random을 사용할 때는 특정 '형식'의 값을 테스트하고자 한다.

 

예를 들어 이메일 필드가 올바른 형식의 데이터를 받아들일 수 있는지 검증하고자 할 때  실제 이메일 주소의 내용은 중요하지 않으며  어떤 무작위 값이든 이메일 형식을 만족하기만 하면 되는것이다.


자 그러면 이제 테스트 데이터를 만들기 위한 모듈을 테스트 패키지 안에 만들어 보자.

 

 

Top Secrets of The Efficient Test Data Preparation - Waverley

Learn the secrets of efficient test data preparation. Get rid of redundant code. Achieve the same results with minimum efforts.

waverleysoftware.com


위 포스팅은 이지 랜덤 깃허브 README 에 나와있는 Articles and blog posts중 하나의 글을 참고해 모듈을 만들것이다.

저 포스팅의 내용을 간략하게 요약해보자면

EntityHelper 
 - 데이터 베이스와의 상호작용을 단순화하는 메서드


EntityFactory
 - 테스트에 필요한 엔티티를 생성하는 고수준의 인터페이스 제공

 - 엔티티 간의 의존성을 자동으로 설정, 필요한 엔티티를 쉽게 초기화할 수 있는 팩토리 메서드를 구현
 - 내부적으로 EntityHelper를 사용할 수 있으며, 테스트 시나리오의 요구 사항에 맞게 엔티티를 맞춤 설정하는 기능 포함

 

 

이렇게 두가지를 기억하고 이제 코드를 작성해보자.


간단하게 


이커머스 상품 도메인을 구성해봤다.

상품 그룹

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(name = "PRODUCT_GROUP")
@Entity
public class ProductGroup extends BaseEntity {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "PRODUCT_GROUP_ID")
    private long id;

    @Column(name = "PRODUCT_GROUP_NAME")
    private String productGroupName;

    @Embedded
    private ProductStatus productStatus;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "BRAND_ID")
    private Brand brand;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CATEGORY_ID")
    private Category category;

}

 
상품 

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(name = "PRODUCT")
@Entity
public class Product extends BaseEntity {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "PRODUCT_ID")
    private long id;

    @Enumerated(EnumType.STRING)
    private ProductStatus productStatus;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "PRODUCT_GROUP_ID", referencedColumnName = "PRODUCT_GROUP_ID")
    private ProductGroup productGroup;

}


브랜드

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(name = "BRAND")
@Entity
public class Brand extends BaseEntity {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "BRAND_ID")
    private long id;

    @Column(name = "BRAND_NAME")
    private String brandName;

}


카테고리

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(name = "CATEGORY")
@Entity
public class Category extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "CATEGORY_ID")
    private long id;

    @Column(name = "CATEGORY_NAME")
    private String categoryName;

}


그 외에도 PRODUCT_STOCK, OPTION_GROUP, OPTION_DETAIL, PRODUCT_OPTION 엔티티들을 구성해봤는데

나머지 엔티티는 깃허브에서 확인하길 바란다.

 

데이터 생성 모듈을 만들때 직관적으로 빠르게 테스트를 진행하기 위해 코드를 구성해봤다.

public class EasyRandomUtils {
    private static final EasyRandom easyRandom;

    static {
        EasyRandomParameters parameters = new EasyRandomParameters()
                .randomize(Long.class, new LongRangeRandomizer(1, 10000000))
                .randomize(String.class, new StringRandomizer(10));
        easyRandom = new EasyRandom(parameters);
    }
    
    public static EasyRandom getInstance() {
        return easyRandom;
    }
    
}



그리고 참고로 easyRandom에서 특정 타입의 필드들의 값들을 제한 할 수 있는데

이런식으로 jeasy의 random api의 Randomizer를 구현하는 클래스를 만들어서
필요한 범위나 조건을 설정해주고 EasyRandomParameters 객체에에 randomize 필드에 넣어주면 된다.

class LongRangeRandomizer implements Randomizer<Long> {
    private final long min;
    private final long max;

    public LongRangeRandomizer(long min, long max) {
        this.min = min;
        this.max = max;
    }

    @Override
    public Long getRandomValue() {
        return min + (long) (Math.random() * (max - min));
    }
}

 

class StringRandomizer implements Randomizer<String> {
    private final int length;

    public StringRandomizer(int length) {
        this.length = length;
    }

    @Override
    public String getRandomValue() {
        int leftLimit = 97; // 'a'
        int rightLimit = 122; // 'z'
        Random random = new Random();
        return random.ints(leftLimit, rightLimit + 1)
                .limit(length)
                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                .toString();
    }
}




이렇게 코드를 만들어 보았고

EntityHelper 는 정말 단순하게 연관관계 상관없이 그냥 해당 엔티티에 무작위로 값을 생성하도록 만들어놨고
EntityFactory 는 EntityHelper를 사용하여 엔티티를 생성 하고  연관관계 매핑을 해준 정도이다.


public class ProductModuleHelper {

    public static ProductGroup productGroupGenerate(){
        EasyRandom instance = EasyRandomUtils.getInstance();
        return instance.nextObject(ProductGroup.class);
    }

    public static Product productGenerate(){
        EasyRandom instance = EasyRandomUtils.getInstance();
        return instance.nextObject(Product.class);
    }

    public static ProductStock productStockGenerate(){
        EasyRandom instance = EasyRandomUtils.getInstance();
        return instance.nextObject(ProductStock.class);
    }

    public static ProductOption productOptionGenerate(){
        EasyRandom instance = EasyRandomUtils.getInstance();
        return instance.nextObject(ProductOption.class);
    }

    public static OptionGroup optionGroupGenerate(){
        EasyRandom instance = EasyRandomUtils.getInstance();
        return instance.nextObject(OptionGroup.class);
    }

    public static OptionDetail optionDetailGenerate(){
        EasyRandom instance = EasyRandomUtils.getInstance();
        return instance.nextObject(OptionDetail.class);
    }

    public static Brand brandGenerate(){
        EasyRandom instance = EasyRandomUtils.getInstance();
        return instance.nextObject(Brand.class);
    }

    public static Category categoryGenerate(){
        EasyRandom instance = EasyRandomUtils.getInstance();
        return instance.nextObject(Category.class);
    }

}

 

 

public class ProductGroupFactory {
    
    public static ProductGroup createProductGroupWithSpecificConditions() {
        ProductGroup productGroup = ProductModuleHelper.productGroupGenerate();
        Brand brand = ProductModuleHelper.brandGenerate();
        Category category = ProductModuleHelper.categoryGenerate();

        productGroup.setBrand(brand);
        productGroup.setCategory(category);

        return productGroup;
    }
}




 



그리고 실제로 데이터가 잘 만들어지는지 간단한 테스트 코드를 통해 알아보자.

@Service
@RequiredArgsConstructor
public class ProductQueryServiceImpl implements ProductQueryService{

    private final ProductGroupRepository productGroupRepository;

    @Override
    public void save(ProductGroup productGroup) {
        productGroupRepository.save(productGroup);
    }

}



이런 서비스 레이어 코드가 있다 치고 위의 테스트 코드를 아래와 같이 작성했다.


@ExtendWith(MockitoExtension.class)
public class ProductQueryServiceImplTest {

    @Mock
    ProductGroupRepository productGroupRepository;
    @InjectMocks
    ProductQueryServiceImpl productQueryService;


    @Test
    public void testSaveProductGroup() {
        ProductGroup productGroup = ProductGroupFactory.createProductGroupWithSpecificConditions();
        stubbingSaveProductGroup(productGroup);
        productQueryService.save(productGroup);
        // 저장이 제대로 이루어졌는지 확인
        assertAll(
                () -> verify(productGroupRepository).save(productGroup)
        );
    }

    private void stubbingSaveProductGroup(ProductGroup productGroup) {
        when(productGroupRepository.save(productGroup)).thenReturn(productGroup);
    }

}



위의 테스트 코드에 ProductGroupFactory를 통해 생성한 productGroup이 있는데 확인해보면 



위와 같이 값들이 알아서 꽂혀있는걸 볼 수 있다.

이제 테스트 데이터를 사용해서 테스트를 편하게 하면 된다.

 

끝~




https://github.com/Ryu-qq/dataGenerator.git