본문 바로가기
가치관 쌓기/개발 돌아보기

엄청 많은 속성을 가진 객체를 테스트해야 한다면 어떻게 해야될까?(with. test Data Builder)

by simplify-len 2022. 9. 9.

https://unsplash.com/photos/qDisWJ9RLHc

 

SETTLEMENT 라는 결제/환불 원장 프로젝트를 진행하면서 이야기했던 Test Data Builder 에 대해서 이야기해볼까 한다.

Test Data Builder(테스트 데이터 빌더)란 어떤 건인가?
테스트 데이터 빌더를 이야기하기 전에 테스트 코드부터 이야기 해보자.

왜 테스트 코드를 작성하고자 할까?

왜 테스트 코드를 작성하고자 할까? 우리는 테스트 코드로서 무엇을 얻기 위해서일까? 
어떤 문맥이냐?에 따라 테스트 코드가 우리에게 전달해주는 것은 다를테지만, 이야기하고 싶은 바를 먼저 말한다면-
안도감이다. 테스트 코드 덕분에 마음의 안정을 얻을 수 있다. 그것은 서비스를 개발하는 클라이언트에게 심리적인 안정감을 준다. 
'그럼 무조건 테스트 코드를 작성해야 되는거 아닌가?' 라는 생각이 드는 사람이 있을 테고,
그럼에도 불구하고 저항이 발생해서 '테스트 코드를 작성하는 비용은 어떻게 감당할거냐?'  라고 생각하는 사람도 있을 것이다.

2가지 입장이 모두 맞다. 그리고 공감한다. 

테스트 코드를 작성하는 것은 일장일단이 존재하고, 이는 서비스의 규모가 점차 커짐에 따라 무시할 수 없게 된다. 과거 엔터프라이즈급 프로젝트를 유지보수하면서 테스트 코드가 있음을 처음에는 안도했었다. 그러나, 그 테스트 코드는 의존성이 두꺼워짐에 따라 결합도도 정비례하는 테스트 코드 때문에 테스트 코드를 관리하는 것도 하나의 업무가 됐던 적이 있다.

또 이런 애매한 상황도 있다.

흔히 정적 메서드 팩토리를 통해 가독성을 부여해 코드의 가독성을 높이기도 한다.

 

class MemberDemoApplicationTests {  
  
    MemberDemo sut;  
  
    @Test  
    void create() {  
     MemberDemo sut = MemberDemo.create(expectedMemberId, "foo", 10);  
        
    }  
}

이런 식으로 말이다.

그러나 이 가독성도 프로덕트 코드에서는 의미가 있을지 몰라도, 다양한 케이스를 테스트 코드로 표현하기 위해서는 한계가 있다.

Test Data Builder란?

사실 테스트 데이터 빌더는 엄청난 것은 아니다. 테스트 코드에서 객체를 만들기 위한 Builder 를 하나 만드는 것을 말한다.

 

class MemberDemoApplicationTests {  
  
    MemberDemo sut;  
  
    ...
    
    public static MemberDemoBuilder builder() {  
        return new MemberDemoBuilder();  
    }  
    private static class MemberDemoBuilder {  
  
        private Long id;  
        private String name;  
        private int age;  
  
        public MemberDemo build() {  
            return MemberDemo.create(id, name, age);  
        }  
  
        public MemberDemoBuilder withId(Long id) {  
            this.id = id;  
            return this;        }  
  
        public MemberDemoBuilder withName(String name) {  
            this.name = name;  
            return this;        }  
  
        public MemberDemoBuilder withAge(int age) {  
            this.age = age;  
            return this;        }  
    }  
}

 

코드만 보면 쉽게 이해된다. 간단하다. 그럼에도 불구하고 이 간단한 Test Builder 가 가져다주는 이점은 무엇일까? 

Test Builder 가 가져다주는 이점은 무엇일까? 

이렇게 코드를 작성하면서 까지 얻을 수 있는 이점은 무엇이고, 왜 테스트 코드에서만 사용을 했을까?

나는 2가지의 이점이 있다고 생각한다.

1. 가독성

위에서 말했던 정적팩토리 메서드가 기억하는가? 정적 팩토리 메서드를 사용하는 이유는 가독성이라 말했다.

TestBuilder 또한 테스트 코드에 가독성을 부여하기 위해서라고 생각한다.

 

MemberDemo sut = builder().withId(1L)  
        .withName("foo")  
        .withAge(10)  
        .build();

 

요즘의 IDE 는 좋아졌기 때문에 어떤 매개변수를 넣어야 하는지 알려주지만, 만약 IDE 가 정보제공을 하지 않는다면- 어떠할까? 어떤 Index 에 값을 넣어야 하는지 알기 힘들 것이다.

하지만 테스트 빌더를 사용한다면 이같은 문제를 쉽게 해결할 수 있다.

또한 이름을 부여할 수 있기 때문에 다음과 같은 코드가 가능하다.

 

MemberDemo sut = builder().withLongId(1L)  
        .withFirstName("foo")  
        .withAge(10)  
        .build();

 

2. 테스트 픽스쳐를 의미있게 만들기 위한 용이성

 

코드부터 살펴보면 다음과 같은 테스트 코드를 많이 삽입하게 된다.

 

Assertions.assertThatThrownBy(() -> MemberDemo.create(null, "foo", 10));  
Assertions.assertThatThrownBy(() -> MemberDemo.create(1L, null, 10));

 

이러한 테스트 코드가 만약 다음과 같아진다면 어떠할까?

 

MemberDemo noIdMember = MemberDemoBuilder.builder()  
        .withName("foo")  
        .withAge(10)  
        .build();  
  
MemberDemo noNameMember = MemberDemoBuilder.builder()  
        .withId(1L)  
        .withAge(10)  
        .build();

 

변수가 3개 밖에 없어서 이것이 어떤 의미인지 와닿지 않을 수 있다. 그러나, 필요한 매개변수만 넣어 테스트 시에 Test Builder 는 유용한 도구가 된다고 생각한다.

 

실제로 어떻게 사용했는가?

결제/환불 원장을 쌓기 위한 프로젝트에서 결제 라는 것은 다양한 형태를 가지게 된다.

멤버십을 결제한 경우.
책을 결제한 경우.
굿즈를 결제한 경우.
그 외

하나의 결제라는 것에는 다양한 아이템이 들어갈 수 있어야 했다.

만약 이것을 정적 메서드 팩터리로 만들었다면, 멤버십은 결제 했지만, 굿즈를 결제하지 않았다면- 굿즈를 불필요하게 Null 로 표현해야 될 수도 있다.

불필요한 코드가 삽입되는 것을 TestCode 에서 Builder 를 만들어 테스트 시 유연성을 제공했다.

 

[참고 자료]

https://4think.tistory.com/entry/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B9%8C%EB%8D%94

댓글