서비스 레이어의 테스트는 어떻게 작성하나요? (feat. 테스트 코드 작성하는 법)
안녕하세요. yeTi입니다.
오늘은 종종 헷갈리는 서비스 레이어의 테스트를 바라보는 관점에 대해 말해보고자 합니다.
발단
Spring 으로 서버 어플리케이션의 구조를 정의하면 많은 경우 Controller - Service - Repository(DAO)
형식의 layered architecture
를 보게 됩니다.
이러한 구조에서 테스트 환경을 구성하게 되면 테스트 효율성을 높이기 위해 slice test
를 하게 됩니다.
여기서 개발자들의 의문이 생깁니다.
Service 테스트를 하려고 하는데요. 데이터가 잘 저장되는지는 어떻게 확인하면 될까요?
이 질문은 테스트를 위한 두 가지 관점이 혼재해서 만들어졌다고 생각합니다.
사용자 관점
우리가 흔하게 정의하는 API
만 사용자라는 대상이 있는게 아니라 클래스
나 함수
또한 그것을 사용할 수 있는 사용자가 존재합니다.
테스트 케이스의 주요 관점은 사용자 관점의 테스트입니다.
OOP(Object-Oriented Programming)
에서는 객체의 상태에 대해 이렇게 얘기합니다.
객체의 상태를 변경하는 것은 객체의 자발적인 행동뿐이다. - 객체지향의 사실과 오해 중
즉, 객체간 협력에 관심을 가지고 객체간 어떻게 협력해 나갈지가 중요한 관점이지 객체의 함수를 호출했을 때, 그 객체 내부의 상태가 어떻게 변경되는지는 관심사 밖이라는 말입니다.
사용자 입장에서 서비스 객체의 협력의 관점에서 보는 테스트 코드는 다음과 같습니다.
class RegistService {
public RegistStatus regist(MemberInfo memberInfo) {
}
}
@Test
@DisplayName("회원가입을 합니다.")
void testRegistService() {
MemberInfo memberInfo = new MemberInfo("예티", "남자", 30);
RegistStatus registStatus = registService.regist(memberInfo);
assertEquals("성공", resistStatus.getCode());
}
RegistService
가 regist()
를 수행한 후 클래스나 데이터의 상태는 관심사 밖입니다. 왜냐하면 데이터에 관심있는 테스트 레이어는 repository test
이기 때문입니다.
제공자 관점
하지만 개발자는 이런게 궁금합니다.
회원가입을 하면 회원 데이터를 저장하고 기본 프로필 데이터도 저장해야하는데 어떻게 검증할 수 있을까?
이런 의문에 쉽게 적용할 수 있는 테스트 방식은 모든 bean
을 load
하는 통합 테스트 환경에서 테스트를 수행하는 것입니다.
class RegistService {
public RegistStatus regist(MemberInfo memberInfo) {
}
}
@SpringBootTest
@Test
@DisplayName("회원가입을 합니다.")
void testRegistService() {
MemberInfo memberInfo = new MemberInfo("예티", "남자", 30);
RegistStatus registStatus = registService.regist(memberInfo);
Member findMember = memberRepository.findById("yeti.tistory.com");
Profile findProfile = profileRepository.findById("yeti.tistory.com");
assertEquals("예티", findMember.getName());
assertEquals("예티", findProfile.getName();
}
저장하고자 하는 데이터도 확인할 수 있어서 명쾌하게 문제를 해결한 것 같습니다.
그러나,
OOP
에서 말하는 메시지 중심의 설계
에 반하는 생각을 하는거 같아 찜찜합니다.
제공자 관점 테스트의 개선
다르게 접근해보겠습니다.
데이터가 아니고 정보
로 생각을 바꿔보고 RegistService
의 내부를 생각해 보겠습니다.
class RegistService {
public RegistStatus regist(MemberInfo memberInfo) {
memberRepository.save(member.of(memberInfo));
profileRepository.save(Profile.of(memberInfo));
}
}
위 regist()
함수의 테스트 관점은 두 가지로 분류할 수 있습니다.
repository가 저장
하는 관점 과 MemberInfo로 각각 Member 와 Profile 정보를 생성
하는 관점입니다.
저장
하는 관점에서는 앞서 언급한 slice test
에서 repository test
가 커버리지를 제공합니다.
그렇다면 정보를 생성
하는 단위 테스트만 작성하면 모든 테스트에 대한 커버리지를 가졌다고 볼 수 있지 않을까요?
그리고 regist()
함수의 커버리지를 달성했다면 DB에 데이터가 저장되는 것을 실제로 확인하지 않더라도 완전하게 구현했다고 말할 수 있지 않을까요?
결론
서비스 레이어 테스트 시 저도 그렇고 다른 개발자분들도 어떻게 테스트를 작성하면 좋을지에 대해 종종 물어봐주십니다.
서비스에서 내가 원하는 데이터가 잘 저장되는지가 궁금하니까요.
저는 이러한 현상을 사용자 관점과 제공자 관점에서의 테스트 작성
을 기본 개념을 가지고 사용자 관점
에서는 인터페이스 중심
으로 테스트를 바라보고, 제공자 관점
에서는 테스트 커버리지의 부족한 부분
을 채워나가면 될것이라고 생각합니다.
기능의 완성을 데이터의 저장 관점에서 바라보지 않아도 됩니다.