Junit
Mockito
둘 다 테스트 코드를 작성할때 사용하는 각각의 프레임워크인거는 알겠는데, 둘 중 하나만 사용해서 테스트해야하는건지? 아예 분리된 개념인지? 아리송했다.
Junit
자바 프로그래밍 언어용 단위 테스트 프레임워크
Mockito
모의 객체를 생성해서 테스트에서 사용할 수 있도록 만들어주는 프레임워크
Java mocking framework
스프링 부트에서는 테스트를 위해 기본적으로 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