이전에 작성한 테스트 픽스처를 어떻게 만드는 것이 좋을까? 라는 글에서는 테스트 케이스마다 일일이 픽스처를 설정하는 대신, 외부 클래스의 팩토리 메서드를 통해 픽스처를 보다 효율적으로 사용하는 방법에 대해서 알아보았다. 그리고 이어서 테스트 픽스처 관련 학습을 이어가던 중, 테스트 픽스처와 관련된 글이나 자료를 찾아볼 때마다 오브젝트 마더(Object Mother) 패턴이라는 용어를 자주 마주치게 되었다. 이에 대해서 궁금해서 조사해보니, 내가 직접 구성한 테스트 픽스처 제공 방식이 오브젝트 마더 패턴을 활용한 여러 방법 중 하나라는 것을 알게 되었다.
그 외에도 테스트 픽스처와 관련된 자료나 레퍼런스를 탐색하면서 클래스를 통해 픽스처를 만드는 방법 외에도 Enum을 통해서도 픽스처를 제공하는 방식에 대한 내용을 찾아볼 수 있었다. 그래서 Enum을 활용해 픽스처를 구성하는 방식에 대해서 학습하고 프로젝트에 적용해본 경험을 공유하고자 테스트 픽스처에 대한 이야기를 이어 나가려 한다.
마틴 파울러 형님의 말씀에 따르면, Thoughtworks라는 프로젝트에서 처음 만들어졌던 오브젝트 마더(Object Mother)는 테스트에 사용되는 예시 객체를 생성하는 데 도움이 되는 일종의 클래스라고 한다. 이러한 오브젝트 마더 패턴은 테스트 데이터를 생성하는 데 자주 사용되는 패턴 중 하나로, 테스트 환경에서는 다양하고 많은 샘플 데이터 즉, 픽스처가 필요하다. 특히 여러 테스트에서 유사한 형식의 픽스처가 필요한 경우가 많은데, 이때 오브젝트 마더 패턴을 사용한다면 테스트 코드의 가독성을 높이고 중복을 줄일 수 있게 된다.
🧐 사실 오브젝트 마더(Object Mother)를 단순하고 쉽게 직역해보면 객체 어머니라는 뜻인데, 이를 조금만 더 생각해보면 객체를 낳는 역할로 생각해볼 수 있으며, 결국 객체를 생성하는 영역에 대한 관리를 해주는 패턴 중 하나라는 것을 유추해볼 수 있었다.
오브젝트 마더 패턴을 사용하려 했던 이유를 거슬러 올라가 본다면, 테스트 케이스마다 일일이 설정해 줘야 하는 픽스처에 대한 비용을 줄이고자 했던 것이 가장 주요하다. 그래서 동일하거나 유사한 픽스처를 반복적으로 사용해야 할 때 오브젝트 마더는 찰떡같이 효율적인 모습을 보여줄 것으로 생각이 된다.
다만, 장점이 있으면 단점도 있듯이 오브젝트 마더를 통해 제공하는 픽스처들에게 테스트 케이스가 강한 결합을 가지게 된다. 또한, 테스트 케이스에 따라 제공할 픽스처 정보가 변경되어야 할 경우 해당 픽스처를 제공하는 곳들을 모두. 살피고 수정해야 하므로 관리 영역이 늘어난다는 부분을 잘 고려해야 한다. 그래서 오브젝트 마더 패턴은 픽스처의 요구사항이 자주 변경되지 않는 상황에서 더욱 효과를 낼 수 있다고 느껴졌다.
👀 알게 모르게 오브젝트 마더 패턴을 사용하고 있었네?
오브젝트 마더 패턴에 대해서 알고나니 이전 글에서 작성한 픽스처 설정 방식도 오브젝트 마더 패턴을 활용한 하나의 예시였음을 알게 되었다.
오브젝트 마더 패턴은 빌더 패턴과 같은 패턴으로도 적용할 수 있고, 그 외에도 JSON, XML, CVS와 같은 데이터 파일을 통해서도 구성할 수 있다고 한다. 하지만 이번에는 Enum을 사용해 오브젝트 마더 패턴을 구현하는 방법을 직접 테스트 코드로 작성해 보려 한다. 사실 Enum 픽스처 구성방식에 꽂혀서 해보는 것이라고 해도 무방하다.
💡 잠시 Enum이 뭔지 간단하게 짚고 넘어가보면, 위키백과에서는 Enum을 다음과 같이 설명하고 있다.
Enum은 요소, 멤버라 불리는 명명된 값의 집합을 이루는 자료형이다. 열거자 이름들은 일반적으로 해당 언어의 상수 역할을 하는 식별자이다. 이를 나는 이해하기 쉽게 서로 연관된 상수들의 집합이라고 이해하고 사용해왔다.
이번에도 이전 글과 동일하게 댓글 등록이라는 시나리오를 가지고 테스트 픽스처를 구성할 것이다. 이번에도 마찬가지로 댓글을 작성하기 위해서는 회원과 게시글이라는 픽스처가 필요하다. 이전에는 댓글 등록과 관련된 테스트 케이스는 다음 3가지로 검증했었는데, 일관성을 위해 이번에도 같은 테스트 케이스를 검증해 보려 한다. 이전과 큰 차이는 나지 않지만 @DisplayName의 내용이나 메서드명을 조금 건드리긴 했다.
댓글 작성시 투표하지 않아도 정상적으로 등록된다.
댓글 작성시 이미지를 첨부하지 않아도 정상적으로 작성할 수 있다.
댓글 수정시 내용 변경과 함께 이미지를 첨부할 수 있다.
🙏 테스트 케이스가 기획한 도메인의 성격을 따라가다보니 투표나 이미지와 같이 부수적인 요인들이 포함되어 있습니다. 이로 인해 픽스처 네이밍이나 테스트 케이스의 퀄리티를 크게 신경쓰지 못해 이해도가 다소 떨어지더라도 양해를 부탁드립니다.
이번에 테스트 코드를 작성한 필자의 개발환경은 다음과 같다.
Enum 타입의 MemberEnumFixture
를 새로 생성해보자.
public enum MemberEnumFixture { }
테스트 케이스를 자세히 살펴보면, 댓글을 등록할 회원과 게시글을 작성한 회원, 총 2가지 종류의 픽스처가 필요하다. 이 두 가지의 픽스처를 다음과 같이 정의할 것이다.
NORMAL_MEMBER_LANGO
: lango라는 일반 회원 사용자NORMAL_MEMBER_SONNY
: sonny라는 일반 회원 사용자
이제 위에서 정의한 픽스처를 MemberEnumFixture
내에 직접 작성해보자.
먼저, lango라는 정보를 가지는 일반 회원(Normal) 픽스처인 NORMAL_MEMBER_LANGO
상수를 정의했다.
다음으로 sonny라는 정보를 가지는 일반 회원(Normal) 픽스처인 NORMAL_MEMBER_SONNY
상수를 정의했다.
이렇게 Enum을 이용해 간단하게 MemberEnumFixture
에 2가지의 픽스처 상수를 만들어보았다. 이제 이 픽스처를 실제 테스트에서 사용하기 위해 생성자와 Getter 메서드를 설정해주고, 픽스처 팩토리 메서드인 createMember()
메서드를 추가로 작성해준다.
🤔 Enum 픽스처를 구성하는데 생성자와 Getter 메서드가 필요한 이유는?
NORMAL_MEMBER_LANGO라는
픽스처를 생성할 때 정의한 상수의 값을 인자로 받아 각 필드를 초기화한다. email에는lango@gmail.com
, password에는langopwd1234
, nickname에는lango
라는 값을 각 필드에 설정하는데, Enum의 요소가 생성되는 시점에 필요한 정보가 제공되어야 하기 때문에 생성자는 필요하다.
그리고MemberEnumFixture
의 각 필드는 private 접근제한자를 가진다. 그렇기에 Getter 메서드 없이는 private으로 선언된 email, password, nickname이라는 필드에 접근할 수 없도록 설정했다. 이를 통해 객체지향 원칙인 캡슐화를 유지하고 외부의 변경을 제한하도록 할 수 있다. 또한,createMember()
라는 팩토리 메서드를 통해 픽스처를 제공하기 위해서는 Getter 메서드가 필수적이다.
createMember()
메서드를 통해 객체 생성 로직을 캡슐화하여 객체 생성을 더욱 유연하고 편리하게 만들 수 있다. 여기까지 앞서 작성한 MemberEnumFixture
의 코드 전문은 다음과 같다.
public enum MemberEnumFixture {
NORMAL_MEMBER_LANGO(
"lango@gmail.com",
"langopwd1234",
"lango"
),
NORMAL_MEMBER_SONNY(
"sonny@gmail.com",
"sonnypwd1288",
"sonny"
),
;
private final String email;
private final String password;
private final String nickname;
MemberEnumFixture(
String email,
String password,
String nickname
) {
this.email = email;
this.password = password;
this.nickname = nickname;
}
public String getEmail() {
return email;
}
public String getPassword() {
return password;
}
public String getNickname() {
return nickname;
}
public Member createMember() {
return Member.builder()
.email(getEmail())
.password(getPassword())
.nickname(getNickname())
.build();
}
}
이렇게 두 종류의 회원 픽스처를 만들어 두니 실제 댓글 테스트 케이스에서는 아래와 같이 사용할 수 있을 것이다. 요 방식대로 픽스처를 구성하면 테스트 케이스에서 회원의 값에 크게 신경쓰지 않고 Enum 픽스처를 호출하는 것만으로 손쉽게 설정할 수 있다.
NORMAL_MEMBER_LANGO.createMember();
NORMAL_MEMBER_SONNY.createMember();
다음으로 Enum 타입의 BoardEnumFIxture
를 새로 생성해보자.
public enum BoardEnumFixture { }
댓글 테스트 케이스를 검증하기 위해서 회원 픽스처와 더불어 게시글 픽스처가 필요하다. 여기서는 서로 다른 회원이 작성한 2개의 게시글 픽스처를 만들어 볼 것이다.
BOARD_VOTE_WRITER_LANGO
: lango 회원이 작성한 투표 게시글BOARD_CHOICE_WRITER_SONNY
: sonny 회원이 작성한 찬반 게시글
이번에도 마찬가지로, 위에서 정의한 픽스처를 BoardEnumFixture
내에 직접 작성해보자.
먼저, lango 회원이 작성한 투표 타입(Vote)의 게시글 픽스처 BOARD_VOTE_WRITER_LANGO
상수를 정의했다.
두 번째로, sonny 회원이 작성한 찬반 타입(Choice)의 게시글 픽스처 BOARD_CHOICE_WRITER_SONNY
상수를 정의했다.
MemberEnumFixture
과는 마찬가지로 BoardEnumFixture
에서도 생성자와 Getter 메서드, 픽스처를 제공할 팩토리 메서드 createBoard() 메서드를 추가로 작성해주면 된다.
🙏
BoardEnumFixture
의 생성자나 Getter 메서드, 픽스처 팩토리 메서드 등의 구성 정보는 앞서MemberEnumFixture
를 만들 때 언급했었기 때문에 스킵한다.
그렇게 작성해본 BoardEnumFixture
의 코드 전문은 다음과 같다.
public enum BoardEnumFixture {
BOARD_VOTE_WRITER_LANGO(
"집에서 입을 잠옷 어떤 것이 좋을까요?",
"잠옷 후보 3개 정도를 추려봤는데 골라주세요!",
NORMAL_MEMBER_LANGO.createMember(),
VOTE,
LocalDateTime.now(),
List.of("image-1", "image-2", "image-3")
),
BOARD_CHOICE_WRITER_SONNY(
"겨울 다 지났는데 발마칸 코트를 살까요? 말까요?",
"겨울 내내 고민이네요. 여러분들의 귀한 조언을 얻고 싶습니다.",
NORMAL_MEMBER_SONNY.createMember(),
CHOICE,
LocalDateTime.now(),
List.of("image-1")
),
;
private final String title;
private final String content;
private final Member writer;
private final CategoryBoard category;
private final LocalDateTime deadLine;
private final List<String> boardImages;
BoardEnumFixture(
String title,
String content,
Member writer,
CategoryBoard category,
LocalDateTime deadLine,
List<String> boardImages
) {
this.title = title;
this.content = content;
this.writer = writer;
this.category = category;
this.deadLine = deadLine;
this.boardImages = boardImages;
}
public String getTitle() {
return title;
}
public String getContent() {
return content;
}
public Member getWriter() {
return writer;
}
public CategoryBoard getCategory() {
return category;
}
public LocalDateTime getDeadLine() {
return deadLine;
}
public List<String> getBoardImages() {
return boardImages;
}
public Board createBoard() {
return Board.builder()
.title(getTitle())
.content(getContent())
.member(getWriter())
.categoryBoard(getCategory())
.deadLine(getDeadLine())
.attachPaths(getBoardImages())
.build();
}
}
이렇게 Enum으로 제공할 게시글 2가지 픽스처를 만들어보았다. 이 또한 아래와 같이 댓글 테스트 케이스에서 사용할 수 있게 된다.
BOARD_VOTE_WRITER_LANGO.createBoard();
BOARD_CHOICE_WRITER_SONNY.createBoard();
BOARD_VOTE_WRITER_LANGO_EMPTY_BOARD_IMAGES.createBoard();
MemberEnumFixture
에서 회원 픽스처를, BoardEnumFixture
를 통해서는 게시글 픽스처를 Enum으로 만들어 설정해보았다. 픽스처 설정이 끝났으니 실제 테스트에서는 이를 가져다 호출하기만 하면 된다.
댓글 테스트 케이스는 댓글 등록 케이스 2가지와 댓글 수정 케이스 1가지로 구성되어 있다. 여기서는 댓글(Comment) 엔티티가 가지는 내부 도메인 성격을 테스트하기 위한 단위 테스트를 작성해볼 것이기 때문에 간단하게 엔티티의 등록 및 수정여부를 검증하였다.
createCommentWhenPickIsEmpty
: 댓글 작성시 투표하지 않아도 정상적으로 등록된다.createCommentWhenImageIsEmpty
: 댓글 작성시 첨부파일을 업로드하지 않아도 정상적으로 등록된다.changeCommentContentWithImageUpload
: 댓글 수정시 내용과 함께 투표를 변경할 수 있다.기존에 팩토리 클래스로 픽스처를 제공하던 테스트 케이스의 픽스처 구문을 고쳐보자. 여기서는 given절만 유심히 보면 된다.
먼저, createCommentWhenPickIsEmpty
테스트 케이스의 픽스처를 설정하였다.
다음으로, createCommentWhenImageIsEmpty
테스트 케이스의 픽스처를 설정하였다. 잘 보면 알겠지만, createCommentWhenPickIsEmpty
와 createCommentWhenImageIsEmpty
의 회원 픽스처는 COMMENT_WRITER_SONNY
를, 게시글 픽스처로는 BOARD_WRITER_LANGO
로 완전히 동일한 픽스처를 사용하였다.
마지막으로, changeCommentContentWithImageUpload
테스트 케이스의 픽스처를 설정하였다. 다만, 이번에는 Sonny 회원이 작성한 게시글에 Lango 회원이 댓글을 다는 방식으로 픽스처를 교체하였다. 위 2개의 테스트 케이스처럼 일관성있게 픽스처를 통일시킬 수도 있었지만, 다른 데이터를 가진 픽스처로도 테스트해보고 싶어 마지막 테스트 케이스의 픽스처는 위와 같이 설정했다.
✅ 테스트 케이스마다 보이는 when절에서의 댓글(Comment) 객체의 경우 픽스처가 아닌 실제 검증해야 할 객체이기 때문에 픽스처와는 구분하기 위해 private 메서드로만 추출해두었다.
이렇게 완성한 CommentTest
테스트 클래스의 풀 코드 전문은 다음과 같다.
class CommentTest {
@DisplayName("댓글 작성시 투표하지 않아도 정상적으로 등록된다.")
@Test
void createCommentWhenPickIsEmpty() {
// given
Member COMMENT_WRITER_SONNY = NORMAL_MEMBER_SONNY.createMember();
Board BOARD_WRITER_LANGO = BOARD_VOTE_WRITER_LANGO.createBoard();
// when
Comment comment = createCommentWithoutPick(COMMENT_WRITER_SONNY, BOARD_WRITER_LANGO);
// then
assertThat(comment.getPick()).isZero();
}
@DisplayName("댓글 작성시 이미지를 첨부하지 않아도 정상적으로 작성할 수 있다.")
@Test
void createCommentWhenImageIsEmpty() {
// given
Member COMMENT_WRITER_SONNY = NORMAL_MEMBER_SONNY.createMember();
Board BOARD_WRITER_LANGO = BOARD_VOTE_WRITER_LANGO.createBoard();
// when
Comment comment = createCommentWithoutAttachment(COMMENT_WRITER_SONNY, BOARD_WRITER_LANGO);
// then
assertThat(comment.getContent()).isEqualTo("1번 잠옷이 더 귀엽네요.");
}
@DisplayName("댓글 수정시 내용 변경과 함께 이미지를 첨부할 수 있다.")
@Test
void changeCommentContentWithImageUpload() {
// given
Member COMMENT_WRITER_LANGO = NORMAL_MEMBER_LANGO.createMember();
Board BOARD_WRITER_SONNY = BOARD_CHOICE_WRITER_SONNY.createBoard();
Comment comment = createComment(COMMENT_WRITER_LANGO, BOARD_WRITER_SONNY, 1);
// when
comment.changeComment("저는 발마칸 코트 너무 이쁜 것 같아요. 구매 추천드립니다!!", "image-02-update");
// then
assertThat(comment.getContent()).isEqualTo("저는 발마칸 코트 너무 이쁜 것 같아요. 구매 추천드립니다!!");
assertThat(comment.getAttachmentUrl()).isEqualTo("image-02-update");
}
private Comment createCommentWithoutPick(Member commentWriter, Board board) {
return Comment.builder()
.member(commentWriter)
.board(board)
.content("1번 잠옷이 더 귀엽네요.")
.attachmentUrl("image-01")
.build();
}
private Comment createCommentWithoutAttachment(Member commentWriter, Board board) {
return Comment.builder()
.member(commentWriter)
.board(board)
.content("1번 잠옷이 더 귀엽네요.")
.pick(1)
.build();
}
private Comment createComment(Member commentWriter, Board board, int pick) {
return Comment.builder()
.member(commentWriter)
.board(board)
.content("저는 발마칸 코트를 입어보질 않았는데, 이번 주에 쇼핑가서 입어보고 댓글로 남겨놓겠습니다!")
.pick(pick)
.attachmentUrl("image-01")
.build();
}
}
이렇게 Enum으로 제공하는 픽스처들로 given절을 모두 변경한 후에 CommentTest
테스트 클래스 테스트를 수행하니 아래와 같이 정상적으로 모든 테스트 케이스가 통과되는 것을 볼 수 있었다.
여기까지 Enum을 활용하여 픽스처를 제공하도록 구성하여 오브젝트 마더 패턴을 적용하는 시간을 가져보았다. 굉장히 단순한 수준이라 부끄럽지만 그럼에도 아래와 같은 부분들을 느낄 수 있었다.
Enum의 불변성 덕분에 정말 걱정 없이 픽스처를 만들고 사용해보았다. 원하는 픽스처 구성을 완료한다면, 픽스처 구성에 많은 신경을 쓰지 않아도 있다고 느껴졌고 여러 테스트 메서드에서 같은 픽스처가 요구된다면 바로 가져다 쓰면 되기 때문에 사용하기도 편했다. 가장 마음에 들었던 점은 given절의 픽스처들이 위 코드 중 하나인 Member COMMENT_WRITER_LANGO = NORMAL_MEMBER_LANGO.createMember();
와 같이 상수처럼 취급하니 when절, then절에서 만들어지는 객체와는 다르게 구분되어 읽혀졌다는 점이다. 그래서 무엇이 픽스처 객체이고 무엇이 검증 객체인지 헷갈리지 않았다.
다만, 별도의 Enum에서 픽스처를 관리하기 때문에 테스트하는 도메인이나 엔티티의 요소에 변경점이 생겼을 경우 관련된 Enum의 픽스처들도 모두 관리의 대상이 된다는 것이 부담이 될 것으로 보여졌다. 또한, 인자값을 다양하게 바꿔가며 픽스처를 만들 수 있는 팩토리 클래스 방식과는 다르게 유동성있는 픽스처를 구성하는 상황에 Enum으로 픽스처를 제공하려면 복잡할 수도 있겠다는 생각도 들었다. 그래서 테스트 케이스에 따라 다양한 속성을 지닌 픽스처가 필요할 경우보다는 특정 테스트 시나리오를 기준으로 두고 꼭 필요한 픽스처들을 시나리오에 맞게 Enum에서 픽스처로 제공하는 방식이 보다 효율적일 것으로 느껴졌다.
이렇게 오브젝트 마더 패턴을 적용하기 위해 이전 글에서는 팩토리 메서드를 지닌 클래스에서 픽스처를 제공하는 방식을 알아보았으며, 이번 글에서는 Enum을 활용해 픽스처를 제공하는 방식을 알아보았다. 물론 그 외에도 오브젝트 마더 패턴을 적용하는 방식은 다양하지만 앞으로 이 두 가지 방식을 주력으로 사용하며 개선해볼 생각이다.
먼저 팩토리 메서드를 지닌 클래스나 Enum을 통해 픽스처를 제공하는 방식 모두 픽스처를 생성하는 구문의 중복을 제거해 준다는 점이 가장 마음에 들었다. 사실 테스트 픽스처 관련 학습을 시작한 것도 테스트 메서드마다 중복되어지는 픽스처 구문 때문이기도 하다.
그리고 클래스를 통해 픽스처를 제공하는 방식은 아무래도 팩토리 메서드의 인자값을 동적으로 조정해 가며 픽스처를 구성할 수 있다는 이점이 크다고 생각이 든다. 예를 들어 사용자의 상태가 다양하게 제공되어야할 상황이고 이 상태를 관리하고 테스트하는 것이 중요하다면, 외부 클래스로 픽스처를 제공하는 방식이 좋을 수 있을 것 같다.
Enum을 활용해 픽스처를 제공하는 방식의 경우 강한 불변성을 가지는 Enum을 쓰다 보니 픽스처가 일정하게 유지된다. 그렇기에 Enum에 정의한 픽스처 하나하나가 어떤 상황을 의미하는지 명확하게 지정할 수 있으며 코드만 보아도 무엇이 픽스처인지 한눈에 알아보기 쉽다고 느껴졌다. 그래서 Enum으로 픽스처를 제공하는 상황은 생각보다 다양한 테스트 케이스에서 사용할 수 있다고 보여진다. 단순하게 코드 레벨에서만 놓고 이야기하자면, 가시성은 클래스를 통한 방식보다 좋다고 생각한다. 그래도 예를 하나 들어보자면, 대부분의 테스트 케이스에서 동일한 픽스처나 변경되지 않는 픽스처가 요구되는 상황에 아주 찰떡같이 사용할 수 있지 않을까?
이번 학습을 통해서 오브젝트 마더 패턴과 테스트 픽스처 구성방식의 중요성에 대해서 다양한 고민을 해볼 수 있었다. 특히, 테스트 픽스처를 클래스를 통해 만들 것이냐, Enum을 통해 만들 것이냐 라는 의문에 결론은 아직 낼 수 없었다. 아니, 결론을 못 내었다. 두 방식 모두 직접 많이 사용해보고 느껴봐야 나만의 테스트 픽스처 구성방식을 확립할 수 있지 않을까 생각된다.
감사하게도 테스트 코드와 관련된 학습 열정이 식지를 않는 것 같다. DCI 패턴을 활용해 @Nested 어노테이션으로 테스트코드를 작성한다던가, 테스트 환경에서 도커 컨테이너를 실행할 수 있는 프레임워크인 TestContainers에 대해서 학습해보는 등 배워서 직접 적용하고픈 테스트 코드 지식들이 아직 많이 쌓여있다. 테스트 코드 관련 포스팅은 앞으로도 계속될 예정이다!
😅 아직 부족한 실력이라 혹시라도 잘못된 내용이 포함되었을 수 있습니다. 잘못된 부분을 공유해주시면 정정하도록 하겠습니다!