Spring Unit Test

SeHun.J·2024년 10월 24일

번거로운 테스트 작성

단위테스트를 작성할 땐, 1.독립적이어야 하고, 2. 격리되어야 하고, 3. 가벼워야하기 때문에 @SpringBootTest 어노테이션을 사용하지 않는다고 배웠습니다. 물론, 여러 곳에서 @SpringBootTest를 사용할 경우 캐싱이 적용되어 그렇게 많이 느려지진 않습니다.

하지만 그 첫번째 컴파일 시간도 긴 편이고, 모든 Bean을 IOC Container가 관리하기 때문에 해당 단위테스트에서만 사용하는 컴포넌트 이외의 파일도 같이 실행되기 때문에 독립적이다고 보기 어렵습니다.

통합 테스트에 가까운 단위 테스트

그래서 단위 테스트에서는 @Autowired를 통한 DI 주입이 불가능한데요.
저는 이러한 의존성 주입을 단위 테스트마다 직접 처리했습니다. 처음에는 각 컴포넌트마다 필요한 의존성이 적어서 큰 문제를 느끼지 못했습니다. 하지만 프로젝트가 점점 커지면서 의존성도 늘어갔고 점점 테스트 작성이 복잡하고 귀찮아서 포기하게 되었습니다.

@DataJpaTest
@ActiveProfiles("test")
@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2)
public class PostServiceTestBackup {
    @Autowired
    private PostRepository postRepository;
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private CategoryRepository categoryRepository;
    @Autowired
    private FileRepository fileRepository;
    @Autowired
    private TempFileRepository tempFileRepository;
    private PostService postService;
    private PostUploadService postUploadService;
    private UserService userService;
    private JwtService jwtService;
    private CategoryService categoryService;
    private FileService fileService;
    private TempFileService tempFileService;
    private FileHandler fileHandler;
    private static final TestConfig testConfig = new TestConfig();

    @BeforeEach
    public void beforeSetUp() {
        jwtService = testConfig.createJwtService();
        userService = new UserService(userRepository, jwtService);
        tempFileService = new TempFileService(tempFileRepository);
        fileHandler = new FileHandler(tempFileService);
        fileService = new FileService(fileRepository, tempFileService, fileHandler);
        postService = new PostService(postRepository, userService, fileService);
        categoryService = new CategoryService(categoryRepository, postService);
        postUploadService = new PostUploadService(postRepository, userService, categoryService, fileService);
    }
    
    @Test
    @DisplayName("게시글 Url 조회 테스트")
    public void getPostByUrl() throws BadRequestException {
        // given
        userService.saveUser(testConfig.adminUser);
        // 사용자 정보를 SecurityContextHolder에 등록함.
        testConfig.updateAuthentication(testConfig.adminUser);
        List<Category> categories = categoryService.updateCategories(createCategory());
        RequestPostDto requestPostDto = RequestPostDto.builder()
                .url("url")
                .categoryId(categories.get(0).getId())
                .title("제목")
                .content("내용")
                .files(Collections.emptyList())
                .isPrivate(false)
                .build();
        postUploadService.savePost(requestPostDto);

        // when
        PostDetail postDetail = postService.getPostByUrl("url");
        System.out.println(postDetail);

        // then
        Assertions.assertThat(postDetail.getPost()).isNotNull();
        Assertions.assertThat(postDetail.getPost().getUrl()).isEqualTo("url");
    }
    ...
}

부끄럽지만, 위의 코드는 제 프로젝트의 단위 테스트 코드입니다. Mock, Stub을 활용하지 않았기에 모든 의존성을 직접 주입하고 있네요.

단순히 PostService 하나를 테스트해야 하는데, 그와 관련된 모든 의존성을 직접 관리하니 말도 안되게 커졌습니다. 사실 이렇게 의존성을 직접 주입하는 시점에서 통합 테스트에 가까운 단위 테스트가 되었습니다.

늦었지만, 지금이라도 위기감을 느끼고 단위 테스트에 대해 더 공부하기로 결정했습니다.

단위 테스트 리팩토링하기

현재 위의 코드에서 문제되는 건 다음과 같습니다.

  1. 모의 객체가 아닌, 실제 객체를 주입하고 있다. 이는 외부 리소스(ex. DB)에도 의존하게 됨. 또한, 상태를 조작하기가 어려워짐.

  2. 의존성 간의 상호작용에 의해 테스트 결과가 영향을 받을 수 있다. 단위 테스트는 하나의 단위를 외부 의존성과 분리하여 테스트하는 것이 목표, 그런데 실제 객체를 주입하면 해당 객체가 가지고 있는 다른 의존성이나 내부 상태 때문에 테스트 결과에 영향을 줄 수 있게 된다.

결국, 의존성을 주입할 때 실제 객체가 아닌 모의 객체를 활용하는게 리팩토링의 관건입니다.

@ActiveProfiles("test")
@ExtendWith(MockitoExtension.class)
public class PostServiceTest {

    @Mock
    private PostRepository postRepository;
    @Mock
    private UserService userService;
    @Mock
    private FileService fileService;
    @InjectMocks
    private PostService postService;
    private static final TestConfig testConfig = new TestConfig();
    
    @Test
    @DisplayName("게시글 Url 조회 테스트")
    void getPostByUrl() throws BadRequestException {
        // given
        testConfig.updateAuthentication(testConfig.adminUser);
        Category category = Category.builder().writeCommentAuth(Role.ADMIN).build();
        Post post = Post.builder().category(category).build();
        given(userService.getUserByEmail(testConfig.adminUser.getEmail())).willReturn(Optional.ofNullable(testConfig.adminUser));
        given(userService.isAdmin(testConfig.adminUser)).willReturn(true);
        given(postRepository.findPostByUrl("url", 0L, true, testConfig.adminUser.getRole())).willReturn(Optional.ofNullable(post));

        // when
        PostDetail postByUrl = postService.getPostByUrl("url");

        // then
        assertThat(postByUrl.getPost()).isEqualTo(post);
    }
    
    ...
    
}

Mock을 활용해서 필요한 의존성들을 전부 모의 객체로 주입하고 테스트하고자 하는 PostService의 로직만 검증하게 되었습니다. 이전에는 의존성 객체들의 메소드까지 작동해야 했기 때문에, 불필요한 데이터가 많이 요구되었고 해당 단위테스트의 주체인 PostService의 로직만 검증할 수 없었던 문제가 어느정도 해결된 것 같습니다.

또한, Repository의 단위 테스트가 아님에도, @DataJpaTest를 사용하여 DB환경까지 세팅했었던 문제가 해결되었네요.

profile
취직 준비중인 개발자

0개의 댓글