단위 테스트 고민과 해결 : DI와 DIP 적극 활용하기
문제 상황
JpaRepsitory의 구현체인 GroupJpaRepository에 직접 의존하고 있는 GroupService를 단위 테스트하고자 한다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class GroupService {
private final GroupJpaRepository groupJpaRepository;
...
}
시도한 것
1. 실제 db를 사용해 통합 테스트 / 외부 Mocking 라이브러리 사용
테스트하고자 하는 대상이 repository 구현체에 의존하고 있기 때문에 실제 repository 구현체를 사용하고 데이터베이스와 연동해 통합 테스트를 하거나, Mockito와 같은 외부 라이브러리를 사용하여 Mock 객체를 사용할 수 가 있다. 하지만 통합 테스트를 한다는 것부터가 단위 테스트가 아니게 되며, 테스트의 속도가 엄청 느려지게 된다. 외부 라이브러리를 사용하여 Mocking하는 것은 테스트 코드의 복잡성을 증가시킨다는 문제가 있다.
해결 방법
의존성 주입(DI)과 의존 역전 원칙(DIP)을 활용하면 실제 데이터베이스나 외부 라이브러리 없이 GroupService를 단위 테스트할 수 있다.
우선 상위 인터페이스인 GroupRepository를 생성하여 기존 Repository 구현체를 캡슐화한다.
public interface GroupRepository {
Group save(Group group);
Group getById(long groupId);
...
}
@Repository
public class GroupRepositoryImpl implements GroupRepository {
@Override
public Group save(final Group group) {
...
}
...
}
그리고 GroupService가 GroupRepository 인터페이스에 의존한다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class GroupService {
private final GroupRepository groupRepository;
...
}
실제 애플리케이션 동작 시에는 스프링 빈으로 등록된 Repository 구현체가 Service로 주입 될 것이다.
이후 테스트에 사용할 Repository를 생성한다.
package com.cukhive.cukhive_backend.group.mock;
import com.cukhive.cukhive_backend.group.domain.Group;
import com.cukhive.cukhive_backend.group.port.out.GroupRepository;
import java.util.HashMap;
import java.util.Map;
import org.springframework.test.util.ReflectionTestUtils;
public class GroupTestRepository implements GroupRepository {
private static Map<Long, Group> storage = new HashMap<>();
private static long sequence = 0L;
@Override
public Group save(Group group) {
++sequence;
ReflectionTestUtils.setField(group, "id", sequence);
storage.put(sequence, group);
return group;
}
@Override
public Group getById(long groupId) {
Group group = storage.get(groupId);
if (group == null) {
throw new IllegalArgumentException();
}
return group;
}
}
TestRepository는 오로지 순수 Java로만 작성되었다. 그리고 GroupRepository 인터페이스를 구현하고 있기 때문에 테스트 대상인 GroupService에 주입될 수 있다.
TestRepository를 Service에 주입하여 단위 테스트를 진행한다.
class GroupServiceTest {
GroupService groupService;
GroupRepository groupRepository;
@BeforeEach
public void init() {
groupRepository = new GroupTestRepository();
groupService = new GroupService(groupRepository);
}
@Test
...
}
이렇게 하면 비교적 간단하게 서비스 레이어를 단위 테스트 할 수 있게 된다. 구조의 변화를 클래스 다이어그램으로 보면 다음과 같다.
설계 변경 후에는 상황에 따라서 적절한 구현체를 주입하고 있다. 이는 SOLID 원칙의 관점에서는 의존 역전 원칙(DIP)를 적용한 것과 같다. DIP를 적용해 Service를 구현체가 아닌 인터페이스에 의존하도록 수정했다. 이를 통해 테스트 대상과 외부 요소 간 의존성을 약화하고, 가볍고 빠른 단위 테스트를 할 수 있다.
알게된 것
앞서 다룬 상황 외에도 LocalDateTime.now나 Random 같이 개발자가 제어할 수 없는 부분에도 똑같은 방식으로 적용하고 단위 테스트를 수행할 수 있을 것이다.
참고