Java에서 테스트 객체를 만드는걸 고민하다가 ObjectMother 패턴에 대해서 공부하게 되었다.
그러고 찾은게 바로 EasyRandom.
mother 패턴을 모른다면 위 페이지에서 개념을 보고가면 좋겠다.

ObjectMother와 EasyRandom을 팀 내에 소개했는데 이를 적용하고 테스트 작성에 대한 효율이 눈에 띄기 좋아졌다.
지금 우리 팀원들은 테스트 작성할 때 무조건 이 방식을 사용하는 정도.

장점

ObjectMother 패턴을 설명하는 페이지에도 적혀 있지만 이 간단한 패턴으로 얻은 명확한 장점들이 있다.

  1. 이곳저곳 산개되었던 객체 생성 로직을 하나로 묶을 수 있었다.
    • test Class/Method마다 객체를 만들던 로직이 있었는데, ItemMother와 같이 mother 패턴을 적용하면서 이런 로직이 사라졌다.
    • Item의 테스트 객체를 만들기 전에 ItemMother가 있는지를 보고 없으면 만들고 있으면 사용하는 방식.
  2. 위와 같은 이유로 테스트 객체 생성에 드는 시간과 비용이 줄어들었고, 이는 테스트를 짜는 시간에 영향을 미쳤다.
  3. 위와 같은 장점으로 완성도 높은 테스트 객체 생성 로직을 갖게 되었다.
    • 원래는 귀찮아서 대충 생성하던 것도 같은 생성 코드를 사용하기 때문에 점진적으로 완성도가 올라간다.
  4. 객체 생성 로직이 숨겨지면서 가독성이 올라갔다.

우리 코드가 어떻게 되었고, 어떻게 바뀌었는지를 예를 들어서 설명해보겠다.
예를 들면서 EasyRandom의 사용 방법도 설명한다.

java에서 테스트 객체 만들기

우리가 테스트에서 만들었던 item 생성 코드들을 보자.

실제 코드 sample 1

    ItemDTO ItemDTO = new ItemDTO();
    ItemDTO.setCtime(0L);
    ItemDTO.setItemId("testItemId");
    ItemDTO.setOwner("testOwner");

문제가 많다.

  • 테스트에서 이런 코드 반복이 많다. 언제까지 이런 코드를 반복할 것인가?
  • 모르고 놓치는 경우보다 귀찮아서 안 하는 경우들이 참 많은데 random value도 아니다.

실제 코드 sample 2

오래된 프로젝트의 테스트 코드에서 심심찮게 보이는 반복 호출을 위한 테스트 함수.

public static ItemDTO createItemDTO() {
    long crrntTime = System.currentTimeMillis();
    Meta meta = MetaTest.create();

    ItemDTO dto = new ItemDTO();
    dto.setItemId(itemId);
    dto.setCtime(crrntTime);
    dto.setMtime(crrntTime);
    dto.setOwner(owner);
    dto.setLastmodifier(owner);

    dto.setMeta(meta.getMeta());
    dto.setStatus(Status.CREATED);

    return dto;
}

아직 문제가 많다.

  • 이런 코드가 프로젝트 곳곳에 있다.
    • 누가 만들었는지, 있는지 기억도 못하기 때문에
    • 심한 경우는 test class 마다 존재하기도 한다
  • 여전히 random value는 아니다.
    • 값을 받아오기엔 random으로 해야할 게 많고, randomUtil로 모두 넣기 귀찮았나보다.

실제 코드 sample 3

private static ItemDTO initTestValue() {
    ItemDTO itemDTO = new ItemDTO();
    itemDTO.setCtime(RandomUtils.nextLong(100L, System.currentTimeMillis()));
    itemDTO.setItemId(RandomStringUtils.randomAlphanumeric(10));
    itemDTO.setOwner(RandomStringUtils.randomAlphanumeric(10));

    return itemDTO;
}

조금은 쓸만하다. 이 정도면 일종의 objectMother라고 할 수 있을 것 같다.
그렇지만

  • 언제까지 random으로 한땀 한땀 넣어줄건지.
  • 생성로직이 산개되어 있고 네이밍이 명확하지 않다는 문제는 여전하다.

EasyRandom

ObjectMother의 역할을 하는 프로젝트들이 찾아보니 몇 개 있었다.
굉장히 유명하고 보편적으로 사용되는 프로젝트들은 아니지만 흥미로웠다.

Naver에서 관리하고 있는 FixtureMonkey 도 있었고,
약간은 올드한 네이밍의 PODAM,
조금 궤가 다르지만 param test에 random하게 값을 채워주는 AutoParams 들을 사용해봤다.

가장 쓰기 편하고 효율적인 프로젝트는 EasyRandom이었다.

  • 이름 값 한다.
  • star 수도 가장 많았다.

EasyRandom은 굉장히 강력한데, 내가 사용하며 확인된 사항은 다음과 같다.

  1. setter가 없어도 된다.
  2. contructor가 없어도 된다. (private contructor only인 경우)
  3. 자동으로 sub class들의 값도 random하게 채워준다.
  4. test object list 생성이 간단하다.

setter와 constructor가 없어도 된다는 점이 굉장히 좋았다.

  • 가독성++

특정 entity의 경우 private consturctor만 갖고 factory에서 생성을 하는데, factory는 또 dto를 받는 번거로운 구조를 갖는 경우가 있었다.
이런 경우 항상 테스트에서 dto 생성 후 entity를 만드는 test 이해도를 떨어뜨리는 작업을 했어야 했다.
EasyRandom은 이런 단점들을 보완해준다.

사용법

우선 dependency를 추가한다.

<dependency>
    <groupId>org.jeasy</groupId>
    <artifactId>easy-random-core</artifactId>
    <version>4.0.0</version>
</dependency>

기본 사용

    EasyRandom generator = new EasyRandom();
    Person person = generator.nextObject(Person.class);
    List<Person> persons = generator.objects(Person.class, 5).collect(Collectors.toList());

굉장히 간단하다.
List(필요하다면 다른 colletions)를 만들기도 쉽다.
위에서 언급한 장점들까지 고려한다면 random value object 생성을 한 줄에!

parameter와 함께 사용

    EasyRandomParameters parameters = new EasyRandomParameters();
    parameters.stringLengthRange(3, 3);
    parameters.collectionSizeRange(5, 5);
    EasyRandom generator = new EasyRandom(parameters);
    Person person = generator.nextObject(Person.class);

아주 간단하게 특정 param이나 value에 조건을 더할 수도 있다.

적용된 코드 예시

public class ItemDTOMother {
  private static ItemDTO generate() {
    EasyRandom easyRandom = new EasyRandom();
    return easyRandom.nextObject(ItemDTO.class);
  }

  private static ItemDTO generateDeleted() {
    ItemDTO item = generate();
    item.setStatus("DELETED");
    return item;
  }
}

이렇게 되면 모두 동일한 생성 로직(XXMother)를 보게 되고 완성도 높고 재사용률 높은 테스트 코드가 완성된다.

reference