단위 테스트에서 테스트 데이터에 builder 패턴 활용 예제

2013-02-27 18:39

단위 테스트할 때 테스트 데이터를 초기화하는 것이 항상 큰 이슈였다. 테스트 데이터를 초기화할 때 중복되는 부분도 많이 발생하고 생각보다 복잡하게 구성할 수 밖에 없는 경우가 많았다. 특히 비슷한 데이터 구조를 매번 반복해야 하기 때문에 여간 관리하기 힘든 것이 아니었다.

이 때 사용한 방법 중의 하나가 다음과 같이 각 도메인을 대표하는 단위 테스트 클래스에 몇 개의 테스트 데이터를 다음과 같이 초기화하는 방법이다.

public class TagTest {
	public static final Tag JAVA = Tag.pooledTag("java");
	public static final Tag JAVA_CHILD = Tag.pooledTag("자바", JAVA);
	public static final Tag JAVASCRIPT = Tag.pooledTag("javascript");
	public static final Tag NEWTAG = Tag.newTag("newTag");
}

이 같은 방식으로도 일정 부분 해결할 수 있었지만 테스트 데이터의 유형이 증가할 때마다 매번 추가해야 하는 번거로움이 있었다. 이와 같이 테스트하던 방식을 다음과 같이 builder 패턴을 활용하도록 소스 코드를 개선하니 좀 더 유연하다는 생각이 든다.

package net.slipp.domain.qna;


import java.util.List;
import java.util.Set;


import net.slipp.domain.tag.Tag;
import net.slipp.domain.user.SocialUser;


import com.google.common.collect.Lists;
import com.google.common.collect.Sets;


public class QuestionBuilder {
	private SocialUser writer;
	private String title;
	private String contents;
	private Set<Tag> tags = Sets.newHashSet();
	private List<Answer> answers = Lists.newArrayList();
	
	public static QuestionBuilder aQuestion() {
		return new QuestionBuilder();
	}
	
	public QuestionBuilder withWriter(SocialUser writer) {
		this.writer = writer;
		return this;
	}
	
	[... 중간생략 ...]
	
	public QuestionBuilder withAnswer(Answer answer) {
		answers.add(answer);
		return this;
	}
	
	public Question build() {
		Question question = new Question(writer, title, contents, tags) {
			@Override
			public List<Answer> getAnswers() {
				return answers;
			}
		};
		
		return question;
	}
}

위와 같이 테스트 데이터를 생성하는 Builder 클래스를 만들고 다음과 같이 사용한다.

@Test
public void 질문한_사람이_같다() throws Exception {
	SocialUser writer = new SocialUser(10);
	dut = aQuestion().withWriter(writer).build();


	assertThat(dut.isWritedBy(writer), is(true));
}


@Test
public void 질문한_사람이_다르다() throws Exception {
	SocialUser writer = new SocialUser(10);
	dut = aQuestion().withWriter(writer).build();


	boolean actual = dut.isWritedBy(new SocialUser(11));
	assertThat(actual, is(false));
}

특히 위 QuestionBuilder에서 유용한 부분은 Answer를 처리하는 부분이다. getAnswers() 메소드의 경우 ORM을 활용하기 때문에 데이터베이스에서 Lazy Loading을 통해 데이터를 가져오도록 구현되어 있기 때문에 테스트 데이터를 구성하려면 getAnswers() 메소드를 매번 Overriding해야 되는 귀찮음이 있는데 위와 같이 Builder를 활용해 해결했더니 이와 관련한 중복을 완전히 제거할 수 있었다. 이 부분은 위 예의 Answer 뿐만 아니라 자동으로 생성되는 ID를 조회하는 경우에도 유용하게 사용할 수 있었다.

Builder를 사용할 경우 구현해야할 부분이 많아지는 단점이 있지만 복잡한 테스트 데이터를 구성할 때 사용하면 유용하게 사용할 수 있을 듯하다. 개인적인 경험으로 본다면 그리 복잡하지 않은 테스트 데이터에 대해서도 사용할 경우 나름 괜찮다는 생각이 들었다. 테스트 데이터에 대한 전체 일관성을 생각한다는 측면에서 본다면 모든 경우에 Builder를 사용하는 것도 그리 나쁘지 않을 것이라 생각한다.

0개의 의견 from FB

5개의 의견 from SLiPP

2013-02-28 09:35

@benghun Type이 다르다면 오버로딩도 좋은 방법이겠네요. 하지만 Type이 같을 경우에는 withTitle(String title), withContents(String contents)와 같이 사용해야 되는데요. 그럴 경우 with 메소드와 withTitle, withContents와 같은 메소드가 혼재해 있을 텐데 이 클래스를 사용하는 입장해서 오히려 혼란스럽지 않을까요? 이 부분은 어떻게 생각하세요? 저도 Builder를 오래 사용하지 않아 이와 관련해서는 경험이 없는지라...

2013-02-28 10:18

@자바지기 저는 크게 게의치 않아요. with에 크게 의존하지도 않는 편이죠. 다만 with를 쓸 수 있는 상황이면 쓰고요. 너무 관례에 의존하기 보다는 맘 편히 코딩하는 편이죠. MeetingBuilder with(Contact c) { ... } MeetingBuilder title(String t) { ... } MeetingBuilder contents(String contents) { ... } MeetingBuilder at(String place) { ... }

다만 유효성 체크가 필요한 정보라면 클래스화하여 오버로딩을 활용할 수도 있다고 생각해요. 보통은 불필요하게 느끼겠지만 저는 마음에 드는데 ...

class Title {
    public Title(String title) {
        if (25 < title.length()) {
            throw new IllegalArgumentException();
        }
        ...
    }
}
class Builder {
    Builder with(Title t) { ... }
    Builder with(Contents c) { ... }
}
2013-02-28 11:50

@benghun 클래스화해서 오버로딩하는 방법 괜찮네요. 의미있는 작업이라고 생각합니다. 하지만 현실에서 적용하려면 개발자들의 벽을 느낄 듯 하네요. 어쩌면 Builder 클래스 만드는 것 자체도 거부하는 개발자들이 많을텐데 거기다 title, contents 같은 정보도 클래스화하라고 하면 완전 잡아 먹을 듯 하네요. ㅋㅋ

매번 많은 것으로 보고 배웁니다. 감사합니다.

의견 추가하기

연관태그

← 목록으로