테스트용 엔티티를 돌려쓰지 말 것!
테스트 코드를 작성하다보면 Mock 객체를 매번 생성해주어야한다.
@DataJpaTest 를 통해 트랜잭션 테스트를 진행하다보면
유독 많이 사용되는 객체가 존재한다.
예로 회원서비스는 대부분의 서비스가 의존하고 있기 때문에 회원 엔티티를 Mocking 하는 경우가 아주 많다.
만약 @BeforeAll 과 같은 메서드를 사용하거나, 테스트 클래스 변수 선언을 통해,
Member 엔티티를 단 하나 생성하고, 테스트 메서드간 돌려 쓴다면,
테스트가 의도치 않게 실패할 수 있다.
또 테스트가 실행되는 순서에 따라 서로 논리적인 의존성이 발생하기도 한다.
그 이유는 다음과 같다.
@DataJpaTest 어노테이션은 내부에 @Transactional 이 붙어서 테스트가 하나 끝나면 롤백이 되도록 설정되어 있다.
다만 여기서 롤백의 대상이 무엇인지 아는게 중요하다. 테스트 db 에 커밋되었던 레코드를 롤백한다.
테스트 케이스가 실패하는 주요 원인은 영속성 컨텍스트도 초기화가 될 것이라 생각하기 때문이다.
영속성 컨텍스트는 일종의 캐시역할을 한다.
트랜잭션이 열려 있을 때, 엔티티에 대한 수정사항이 생기면 캐시를 수정하고, 수정할 때마다 엔티티에 새로운 버전을 부여해서 관리한다. 트랜잭션에 오류가 없이 잘 마무리되면 마지막에 db 에 변경사항을 반영(commit)하고 트랜잭션을 닫는다. 영속성 컨텍스트는 트랜잭션과 생명주기가 동일하므로 트랜잭션과 같이 종료된다. 영속성 컨텍스트에서 관리되던 엔티티들은 detach 상태로 표시되게 된다.
또 다른 트랜잭션 테스트가 실행되면, 트랜잭션이 생성되고, 새로운 영속성 컨텍스트가 생성된다. 이 때 전에 사용하던 detach 된 엔티티를 아무생각 없이 재사용하게 되면, 실제로는 비지니스 로직에 문제가 없어도 테스트에 실패할 수 있다. detach 된 엔티티는 merge를 명시적으로 호출하여 새로운 영속성 컨테스트가 관리하도록 해주어야하기 때문이다.
코드를 통해 살펴보자
class TestMember {
public static Member WRITER = new MEMBER(12171652, "홍길동"); // 테스트용 엔티티
}
@DataJpaTest
class BoardRepositoryTest {
@Autowired
private BoardRepository boardRepository;
@Autowired
private TestEntityManager em;
private Member writer = TestMember.WRITER; // static 변수를 그대로 대입.
@DisplayName("게시글을 수정한다.")
@Test
public void update() {
//given
Board board = new Board
.id(1)
.title("제목입니다.")
.contents("내용입니다.")
.writtenBy(writer)
.build();
boardRepository.save(board);
//when
board.modify("제목이 수정되었습니다.", "내용이 수정되었습니다.", writer.getId());
boardRepository.save(board);
//then
Board updatedBoard = boardRepository.findById(FREE_BOARD.getId())
.orElseThrow(EntityNotFoundException::new);
assertThat(updatedBoard.getContents()).isEqualTo("내용이 수정되었습니다.");
assertThat(updatedBoard.getTitle()).isEqualTo("제목이 수정되었습니다.");
assertThat(updatedBoard.getWriterId()).isEqualTo(writer.getId());
}
}
위와 같은 경우에 게시글 수정 테스트만 단건 실행시킨다면 당연히 테스트는 통과할것이다.
하지만 만약 다음과 같은 테스트가 존재한다고 가정해보자.
@DataJpaTest
class MemberRepositoryTest {
@Autowired
private MemberRepository memberRepository;
@DisplayName("회원 이름을 성공적으로 변경한다.")
@Test
public updateMemberNameTest() {
//given
String oldName = TestMember.WRITER.getName(); // "홍길동"
String newName = "고길동";
//when
TestMember.WRITER.setName(newName);
memberRespository.save(WRITER);
//then
Member updatedMember = memberRepository.findById(TestMember.WRITER.getId());
assertThat(updatedMember.getName()).isEqualTo(newName);
}
}
회원 이름을 변경하는 테스트코드가 성공적으로 통과하고 나서,
전체 테스트를 돌릴 때,
회원테스트가 먼저 실행된 뒤에, 게시글 테스트가 실행된다고 해보자.
회원 테스트가 성공적으로 통과하고 나면 TestMember.WRITER 는 detached 된 상태의 엔티티이다.
그 후에 게시글 테스트에서 해당 엔티티를 다시 사용하여 save 를 하게 되면 해당 엔티티를 merge 하지 않았기 때문에 오류가 발생한다.
그리고 여러 테스트를 거칠수록 TestMember.WRITER 의 상태와 내부 변수 값이 계속 변하기 때문에, 테스트 실행 순서에 따라 다른 결과값들을 얻게 된다. (여러 테스트를 한꺼번에 실행시킬 때, 그 순서가 일정하게 보장되지 않는다.)
이를 방지하기 위해서는 팩토리 메서드를 사용하는 방법이 있다.
class TestMember {
// public static Member WRITER = new MEMBER(12171652, "홍길동"); // 변경 전
public static Member WRITER() {
return new MEMBER(12171652, "홍길동"); // 변경 후
}
}
위와 같이 사용하면 값은 같지만 엔티티는 매번 달라지므로 언급했던 문제를 회피할 수 있다.
'웹 프로젝트 (IBAS) > SpringBoot api 개편' 카테고리의 다른 글
[SpringBoot] 9. OSIV 설정을 통한 쿼리 최적화 방안 고찰 (0) | 2022.08.02 |
---|---|
[SpringBoot] 8. 하이버네이트 원격서버 암호화 연결 (SSH tunneling 설정) (4) | 2022.07.30 |
[SpringBoot] 7. SpringSecurity 인증 모듈 개발 (OAuth2, jwt, 소셜로그인) (4) | 2022.07.02 |
DDD, 어그리게이트 분리를 위한 리팩토링 (0) | 2022.06.27 |
OAuth2 naver 회원 id 형식 문제 (네아로) (0) | 2022.05.14 |