프로젝트를 진행하면서 테스트 코드에 대한 필요성 대해 더욱 자각하게 되었다.
그 이유로는
- git Action을 통해 클라우드에 서버가 배포되기 때문에 Github에 푸쉬한 이 후에 배포 환경에서 예상치 못한 에러가 발생하는 것을 경험하게 되었다.
- 각 서버(혹은 컨테이너) 간 상관관계가 일부 존재하기 때문에 서버 간 정보를 주고 받는 경우를 테스트 하기 위한 방법이 필요하였다.
따라서, 테스트 코드를 상세히 작성하여 테스트 코드만으로 "1) 버그와 결함을 조기에 발견, 2) 변경 사항에 대한 동작 확인, 3) CI/CD에서 발생하는 예기치 않은 문제 해결" 이라는 장점을 얻고자 단위 테스트와 통합 테스트에 대해 정리하고자 한다.
단위 테스트(Unit Test)
단위 테스트란 애플리케이션의 작은 단위(일반적으로 메서드)를 사용하고, 나머지 부분은 격리하여, 예상대로 동작하는 지 확인하는 것을 의미한다.
단위테스트의 장점
- 각 기능 단위를 고립시켜 동작을 확인하기 때문에 문제 발생 시 문제 지점을 정확하고 빠르게 파악할 수 있다.
- 단위 테스트로 인해 비즈니스 로직에 대한 변경이 쉽다. 왜냐하면, 코드를 고치더라고 유닛 테스트를 통해 동작이 정상적으로 수행되는 지를 빠르게 파악할 수 있기 때문이다.(회귀 테스트, Regression Testing 이라고도 한다.)
- 기능 단위로 동작을 확인하였기 때문에 작은 부분을 검증하였으므로, 해당 단위들을 합쳐서 다시 검증하는 통합테스트에서 재활용할 수 있다.
통합 테스트(Integration Test)
통합 테스트란, 애플리케이션 내부에 존재하는 구성요소들이 상호작용 및 동작을 정상적으로 수행하는 지 확인하는 것을 의미한다.
통합테스트와 단위 테스트의 큰 차이점은 격리를 하지 않는다는 점에 존재한다. 따라서, 분리된 구성요소가 잘 작동하고 있는지 확인하고자 할 때 활용할 수 있다.
주로 애플리케이션의 큰 단위의 동작 검증, 데이터 베이스 동작 테스트, 외부 API 연동 테스트 등에 활용한다.
추가
Given-When-Then 패턴
많은 개발자가 한번쯤은 들어본 적 있을 것이다. 마틴 파울러의 링크에도 해당 내용을 확인해볼 수 있다.
https://martinfowler.com/bliki/GivenWhenThen.html
글을 읽고 본인이 이해한 내용은 아래와 같다.
Given : 테스트에서 동작을 시작하기 전 전제 조건
When : 사용자가 확인하고자 하는 동작
Then : 동작을 수행함으로써 발생한 변경 사항
Mockito
우선 Mock이란 실제 객체를 대신하여 행동하는(가짜 동작을 수행) 객체 또는 구성요소를 의미한다. 주로 단위 테스트에서 사용되어, 테스트 대상 이외의 요소를 격리한 채로 테스트 하는데 활용될 수 있다.
테스트 대상 이외의 기능은 정상 동작한다는 가정 하에 Mock 객체를 생성하여, 격리한 채로 테스트 대상의 동작을 확인하는 것
Mockito는 Java에서 Mock 객체를 생성하고 사용하는 것을 지원하는 라이브러리이다. Java에서 단위 테스트 시 Mockito를 통해 Mock 객체를 생성하고, 수행할 동작을 설정하며, 해당 객체가 활용되었는지 검증할 수 있다.
Spring Boot 적용
위의 학습내용을 채팅 서버에 적용시켜보았다.
ChatService의 기능을 테스트하기 위해 Mock을 사용하였다. 이로인해 ChatRepository -> MongoTemplate -> MongoConfig의 의존성을 고려하지 않고 간단하게 테스트 가능하다.
JwtTokenProvider의 경우에도 Mock 객체를 통해 격리하였다.
Mock을 사용하게 되면서, when 부분에 Mock객체의 동작도 추가적으로 구성하게 되었다. (격리된 객체의 동작이므로 given에 들어가야하는 건지 정확하게 잘 모르겠다.)
public class ChatServiceTest {
private ChatService chatService;
@Mock
private ChatRepository chatRepository;
@Mock
private JwtTokenProvider jwtTokenProvider;
@BeforeAll
public void setUp() {
MockitoAnnotations.openMocks(this);
chatService = new ChatService(chatRepository, null, jwtTokenProvider);
}
@Test
public void 채팅_저장_Mock() {
// given
String jwtToken = "example_token";
ChatRequest mockChatRequest = new ChatRequest( 1L, "user123","TEXT", "test123");
Chat mockChat = new Chat("1", 1L, "user123", "TEXT", "test123", LocalDateTime.now());
// when
when(jwtTokenProvider.getUserInfo(jwtToken)).thenReturn("user123");
when(chatRepository.save(any())).thenReturn(mockChat); // 반환할 적절한 Chat 객체 설정
Chat savedChat = chatService.saveChat(jwtToken, mockChatRequest);
// then
assertNotNull(savedChat);
assertEquals("user123", savedChat.getSenderId());
assertEquals(mockChat, savedChat);
}
}
위 테스트 코드를 동작함에 따라 테스트 성공 코드를 확인하였다.
이 외에도 성공 케이스/실패 케이스를 골고루 작성하며, 테스트 커버리지를 높이도록 작성함으로써 테스트의 완성도를 높일 수 있다.
단위 테스트와 통합테스트에 대해 공부하면서, 기능 구현함에 있어서 테스트 코드 구현하며 진행해야겠단 생각이 들었다.
이를 통해 높은 테스트 커버리지를 충족시킬 수 있는 테스트 케이스들을 작성할 수 있으며, 애플리케이션을 실행시켜 보지 않더라도 빠른 속도로 기능을 확인할 수 있을 것이다.
'백엔드' 카테고리의 다른 글
Spring Caffeine Async Cache (0) | 2024.04.22 |
---|---|
gRPC통신에 관하여 (0) | 2023.08.07 |
Jackson 라이브러리를 활용한 (역)직렬화 다형성 - JsonTypeInfo / JsonSubTypes (0) | 2023.05.16 |
Kafka-(de)Serialize 에러, ErrorHandlingDeserializer (0) | 2023.03.29 |
Jar VS War (0) | 2022.11.15 |