[인용 및 참고 출처]
1. 구글 검색: SLF4J Manual, "Spring Slf4j docs", SLF4J user manual, (2025.01.28)
2. 단행본: "테스트 코드 작성하기", 장정우, 『스프링 부트 핵심 가이드』, 위키북스, 2022, 177쪽, (2025.01.28)
3. 구글 검색: Spring, "WebMvcTest docs", Annotation Interface WebMvcTest, (2025.01.28)
4. 사전: "mock", "facade", 네이버 영어사전, (2025.01.28)
5. 단행본: "디자인 패턴", 장정우, 『스프링 부트 핵심 가이드』, 위키북스, 2022, 22쪽~24쪽, (2025.01.28)
1. 느긋하게 테스트 코드를 공부했다. [깃허브 링크] [트러블슈팅 링크]
package org.example.expert.domain.todo.controller;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import lombok.extern.slf4j.Slf4j;
import org.example.expert.SharedData;
import org.example.expert.common.config.SecurityConfig;
import org.example.expert.common.exception.notfound.TodoNotFoundException;
import org.example.expert.common.filter.JwtFilter;
import org.example.expert.common.utils.JwtUtil;
import org.example.expert.domain.todo.dto.response.TodoResponseDto;
import org.example.expert.domain.todo.service.TodoService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
@Slf4j
@Import({JwtUtil.class, JwtFilter.class, SecurityConfig.class})
@WebMvcTest(value = TodoController.class)
class TodoControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private TodoService todoService;
@WithMockUser(username = "test", password = "te$R123%XWst4", roles = {"USER", "ADMIN"})
@DisplayName("할 일 단건 조회 - 성공: 유효한 ID로 조회")
@Test
void todo_단건_조회에_성공한다() throws Exception {
// log for @WithMockUser
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
log.info("----------------------------------------------------");
log.info("Username: {}", userDetails.getUsername());
log.info("GrantedAuthority: {}", userDetails.getAuthorities());
log.info("Password: {}", userDetails.getPassword());
// given
TodoResponseDto responseDto = new TodoResponseDto(
SharedData.TODO,
SharedData.USER_RESPONSE_DTO
);
long todoId = responseDto.getId();
String title = responseDto.getTitle();
String contents = responseDto.getContents();
String weather = responseDto.getWeather();
long userId = responseDto.getUser().getId();
String email = responseDto.getUser().getEmail();
String createdAt = responseDto.getCreatedAt().toString();
String updatedAt = responseDto.getUpdatedAt().toString();
// when
when(todoService.getTodo(todoId)).thenReturn(responseDto);
// then
mockMvc.perform(get("/todos/{todoId}", todoId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(todoId))
.andExpect(jsonPath("$.title").value(title))
.andExpect(jsonPath("$.contents").value(contents))
.andExpect(jsonPath("$.weather").value(weather))
.andExpect(jsonPath("$.user.id").value(userId))
.andExpect(jsonPath("$.user.email").value(email))
.andExpect(jsonPath("$.createdAt").value(createdAt))
.andExpect(jsonPath("$.updatedAt").value(updatedAt));
}
@WithMockUser
@DisplayName("할 일 단건 조회 - 실패: 존재하지 않는 ID로 조회 시 예외 발생")
@Test
void todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다() throws Exception {
// log for @WithMockUser
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
log.info("----------------------------------------------------");
log.info("Username: {}", userDetails.getUsername());
log.info("GrantedAuthority: {}", userDetails.getAuthorities());
log.info("Password: {}", userDetails.getPassword());
// given
long todoId = 2L;
// when
when(todoService.getTodo(todoId))
.thenThrow(new TodoNotFoundException());
// then
mockMvc.perform(get("/todos/{todoId}", todoId))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.errorCode").value("ERRNF02"))
.andExpect(jsonPath("$.errorMessage").value("Todo is not found."));
}
}
월요일에 계획한 대로 다양한 책과 문서를 읽다가 머리를 식히고 싶어서 저녁에는 테스트 코드 실패 문제 해결 과정을 천천히 기록했다. 트러블슈팅(troubleshooting)을 작성하면서 겸사겸사 테스트 코드도 다시 공부했다. 아침에 일찍 일어나서 공부하고 일찍 잠들려고 했으나, 이상하게 머리는 밤에 잘 돌아갔다. 다른 사람들이 잘 때 혼자 공부한다는 사실이 나도 모르게 마음에 들어서 그런가. 마감일도 없겠다, 천천히 곱씹듯이 공부했다.
(1) @WebMvcTest: 웹 요청과 응답을 테스트할 때 사용된다. 테스트할 클래스만 가져와서 테스트를 수행하며, 대상 클래스를 명시하지 않으면 @Controller, @RestController, @ControllerAdvice와 같은 컨트롤러 관련 빈(bean) 객체가 전부 자동으로 Spring의 ApplicationContext에 등록된다. 보통 @SpringBootTest보다 가벼운 테스트에 쓰인다.
/**
* Specifies the controllers to test.
* == 테스트할 컨트롤러를 지정한다.
*
* May be left blank
* if all '@Controller' beans should be added to the application context.
* == 이 값을 비워두면, 모든 @Controller 빈이 애플리케이션 컨텍스트에 추가된다.
*
* @return the controllers to test
* == 테스트할 컨트롤러를 반환한다.
*/
@AliasFor("value")
Class<?>[] controllers() default {};
테스트할 클래스(class)를 명시하지 않을 시에는 @Controller 관련 빈(bean) 객체가 전부 애플리케이션 컨텍스트(ApplicationContext)에 등록된다는 설명은 @WebMvcTest 안에 있는 controller() 메서드(method) 정보에서도 확인할 수 있다.
(2) @MockBean: 단어 'mock'은 '거짓된, 가짜의, 모의의'를 의미한다. 예를 들어 칵테일과 비슷하지만 알코올이 없는 무알코올 칵테일을 'mocktail'이라고 한다. 단어 뜻에서 유추할 수 있다시피 이 어노테이션(annotation)은 실제 빈(bean) 객체가 아니라 가짜 객체를 생성해서 주입한다. 이 어노테이션이 선언된 객체는 가짜이므로 실제 행위를 수행하지 않는다. 즉, 개발자가 별도로 동작을 정의해주어야 한다.
// [테스트 1] todo_단건_조회에_성공한다()
when(todoService.getTodo(todoId)).thenReturn(responseDto);
// [테스트 2] todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다()
when(todoService.getTodo(todoId)).thenThrow(new TodoNotFoundException());
예를 들어, 첫 번째 테스트에서는 일정(todo) 식별자에 해당하는 일정을 조회하는 todoService.getTodo(todoId) 메서드가 호출될 때 responseDto 객체를 반환하도록 설정했고, 두 번째 테스트에서는 동일한 메서드가 호출되면 TodoNotFoundException 예외를 던지도록 설정했다. 이처럼 Mock 객체는 실제 동작을 수행하지 않으므로, 개발자가 원하는 동작을 직접 정의해야 한다.
(3) @Test: 테스트 코드임을 나타내는 어노테이션이다. 단위 테스트를 지원하는 JUnit Jupiter에서는 이 어노테이션을 감지해서 테스트 계획에 포함시킨다.
(4) @DisplayName: 테스트 메서드 이름이 복잡하거나 길어서 가독성이 떨어질 때 사용하면 유용하다. 이 어노테이션을 사용해 어떤 테스트인지 간결하고 명확하게 표현할 수 있다.
2. 왜 이름에 4가 들어가는지 궁금해서 찾아본 @Slf4j
(1) 의미: 'Simple Logging Facade for Java' 또는 '간단한 자바 로그 기록 인터페이스(interface)'
(2) @Slf4j의 facade(퍼사드)란?: 디자인 패턴은 크게 생성 패턴, 구조 패턴, 행위 패턴으로 나눌 수 있는데, 그중 퍼사드 패턴은 구조 패턴에 속한다. 구조 패턴은 '객체를 조합하여 더 큰 구조를 만드는 패턴'으로, 퍼사드 패턴은 '서브 시스템의 여러 인터페이스를 하나의 통합된 인터페이스를 제공하는 패턴'이다.
단어 'facade'는 '건물의 정면, (실제와는 다른) 표면'을 의미하므로, 퍼사드 패턴은 '복잡한 내부를 감추고 외부에는 단순한 표면만을 제공하는 디자인 패턴'이라고 이해할 수 있다.
정리하자면, @Slf4j는 java.util.logging, log4j 1.x, reload4j, logback과 같이 여러 로깅 프레임워크(logging framework)를 하나의 인터페이스로 묶어, 개발자가 로그 기록을 일관되게 처리할 수 있도록 돕는 어노테이션이라고 할 수 있다.
'끝을 보는 용기' 카테고리의 다른 글
Day 116 - 휴식 후 JVM을 간단하게 복습하다. (0) | 2025.01.30 |
---|---|
Day 115 - '왜 생겼을까?' 계속 물어보며 TCP와 HTTP를 공부하다. (0) | 2025.01.29 |
Day 113 - 플러스 프로젝트 과제 제출, 예비 개발자로서 휴일을 보내는 가장 완벽한 방법 (0) | 2025.01.27 |
Day 112 - 플러스 프로젝트 리드미(README) 작성 중, 무기력이 무기력해지도록 애쓰는 중 (0) | 2025.01.26 |
Day 111 - 휴식 (0) | 2025.01.25 |