[Spring Boot] Service Repository 계층의 통합 테스트 코드 작성 후기

hellonayeon·2021년 12월 7일
0
post-thumbnail

Junit vs Mockito

Junit Mockito 둘 다 테스트 코드를 작성할때 사용하는 각각의 프레임워크인거는 알겠는데, 둘 중 하나만 사용해서 테스트해야하는건지? 아예 분리된 개념인지? 아리송했다.

  •   Junit  
    자바 프로그래밍 언어용 단위 테스트 프레임워크

  •   Mockito  
    모의 객체를 생성해서 테스트에서 사용할 수 있도록 만들어주는 프레임워크 Java mocking framework

Spring Boot Starter Test

스프링 부트에서는 테스트를 위해 기본적으로 org.springframework.boot:spring-boot-starter-test 라이브러리가 추가된다.

테스트 환경 설정

지난번에 Service 계층의 단위 테스트 코드를 작성했다. 순수하게 Service 계층의 동작에 대해서 테스트 하는 코드라 의존 관계가 있는 객체들은 Mock 객체로 선언해서 사용했다. @ExtendWith(MockitoExtension.class)는 테스트 클래스에서 Mockito 프레임워크를 사용하겠다는 의미다. 실제 스프링 환경 위에서 테스트하지 않겠다는 의미같다.

@ExtendWith(MockitoExtension.class)
class ArticleServiceTest {

    @Mock
    ArticleRepository articleRepository;
    ...
}

이번 Service ➡️ Repository 통합 테스트에서는 두 계층의 플로우를 테스트하기 때문에 스프링 환경에서 테스트를 진행했다. 테스트 환경 설정을 위해 사용한 어노테이션은 다음과 같다.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ArticleIntegrationTest {

    @Autowired
    ArticleService articleService;
    
    ...
    
}
// 스프링 부트의 내장 서블릿 컨테이너(톰캣)을 랜덤 포트로 띄운다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

// 테스트 인스턴스의 생명 주기 설정
@TestInstance

// 객체가 테스트 클래스 단위로 라이프 사이클을 가진다.
@TestInstance(TestInstance.Lifecycle.PER_CLASS)

// 객체가 테스트 메서드 단위로 라이프 사이클을 가진다.
@TestInstance(TestInstance.Lifecycle.METHOD)

테스트 코드를 작성하며 깨달은 사실이 있다. 다른 사람들이 작성한 Service 계층의 코드들을 보면, 메서드에서 도메인 객체를 리턴하거나 아이디 값을 리턴하도록 작성되어있다. 나는 실제로 이 값은 쓰지도 않는데 왜 리턴하지 하면서 void로 바꿔왔는데.. 반환해주는 값이 없으면 테스트 코드 작성이 할 수 없다. 객체가 잘 만들어 졌는지, 올바른 값을 가지고 있는지 확인하기 위해서는 Service 계층의 메서드에서 로직을 수행하고 반환되는 결과 객체를 받아야한다. 이번에 테스트 코드를 작성하며 이런 부분들을 수정했다.

수정전

@Transactional
public void createArticle(...) { ... }

수정후

@Transactional
public Article createArticle(...) { ... }

Service 계층에서 객체를 데이터베이스에 저장하면서 @OneToMany 연관관계를 명시한 객체를 어떻게 저장하는게 좋을지 생각한 적이 있다. 도메인들 각각 그들의 Repository에 저장을 해줘야하나, @OneToMany에서 One에 해당되는 객체의 멤버 변수로 참조시키고 One 만을 명시적으로 저장해줘야하나 고민했다. 처음에는 전자로 구현했었고 테스트코드를 작성하던 도중 오류를 맞닥드려 후자로 변경했다. Service 로직을 수행 후 반환되는 객체에는 Many에 대한 정보가 없었기 때문.. 이 부분은 JPA와 관련이 있고 데이터베이스 쿼리를 수행하기 때문에 성능과 관련있을테니 따로 공부가 필요해보인다.

수정전

    @Transactional
    public void createArticle(User user, String text, LocationRequestDto locationRequestDto, List<String> tagNames, List<MultipartFile> imageFiles) {
        locationDataPreprocess.categoryNamePreprocess(locationRequestDto);
        Location location = locationRepository.save(new Location(locationRequestDto, user.getId()));

        Article article = new Article(text, location, user);

        articleRepository.save(article);

        for(String name : tagNames) {
            tagRepository.save(new Tag(name, article, user.getId()));
        }

        for(MultipartFile multipartFile : imageFiles) {
            String url = fileProcessService.uploadImage(multipartFile, FileFolder.ARTICLE_IMAGES);
            imageRepository.save(new Image(url, article));
        }
    }

수정후

    @Transactional
    public Article createArticle(User user, String text, LocationRequestDto locationRequestDto, List<String> tagNames, List<MultipartFile> imageFiles) {
        locationDataPreprocess.categoryNamePreprocess(locationRequestDto);
        Location location = locationRepository.save(new Location(locationRequestDto, user.getId()));

        Article article = new Article(text, location, user);

        for (String name : tagNames) {
            article.addTag(new Tag(name, article, user.getId()));
        }

        for (MultipartFile multipartFile : imageFiles) {
            String url = fileProcessService.uploadImage(multipartFile, FileFolder.ARTICLE_IMAGES);
            article.addImage(new Image(url, article));
        }

        return articleRepository.save(article);
    }

[전체 코드] 기분 좋은 초록 동그라미 동글동글 🙃💚

생각정리

단위 테스트는 가능한 여러 입력 값들을 넣어보며 정상 케이스 비정상 케이스를 만들어 테스트를 진행했으며 완전히 분리된 모듈을 테스트하는 느낌이었다면 통합 테스트는 각 계층들이 정상적으로 동작해서 결과적으로 바른 아웃풋을 낼 수 있는지 큰 흐름을 테스트하는 느낌이었다. 이번 통합 테스트 코드를 작성하며 통합 테스트 코드는 꼭 Spring 환경 위에서 해야할까? 라는 의문이 생겼다. Mockito 환경해서 가능이야 하겠지만 많은 가짜 객체들을 만들어 줘야 하기 때문에 실제 동작을 테스트 하는 것 보다 가짜 객체를 만드는데 시간이 많이 걸리지 않을까, 그래서 스프링 환경에서 통합 테스트를 진행하는게 아닌가 라는 생각이 든다. 사실 아직 테스트 코드 작성법에 대해 백지인 상태라 나중에 정확한 답을 얻을 수 있길 바란다🤨

통합 테스트에서 반례 값을 넣어 거짓의 결과를 나오게 할 필요가 없는걸까? 단위 테스트에서 비정상 케이스를 일부러 만들었던 것 처럼 말이다. 이건 작성하는 사람 마음일 것 같다. 테스트 코드는 작성한 로직이 어떠한 입력값이 주어졌을때 내 예상대로 동작하는지를 확인하기 위함이니까! 이런 값이 들어갔을 때는 비정상 결과가 나와야한다는 것을 확인해 보고 싶다면 그렇게 짜면 된다. 근데 아무래도 단위 테스트를 촘촘하게 했다면 통합 테스트에서는 정상 결과가 나올 것을 기대해 볼 수 있지 않을까? 행복회로 돌리지 말고, 나를 너무 믿지 말자.

참고문서

📌 에디의 기술블로그. 주니어 개발자를 위한 단위테스트 샘플 코드 소개, 07 Jul 2019

📌 기(술) 블로그. JUnit 의 @TestInstance, 31 Jan 2021

0개의 댓글