요청 및 응답을 처리
해요.유효성 검증
기능을 해당 계층에서 해요.컨트롤러 기능
을 제공해요.@Controller
, @RestController
기능을 사용해서 이를 표현해요.이때 @Controller
는 뷰를 반환할 때 사용하고 @RestController
는 데이터를 반환할 때 사용하는 것을 인지하고 진행할게요.
public class AuthController {
@PostMapping("/save")
public ApiResponse save(@RequestBody User user) {
if (userService.getUser(user.getUserName()) != null) {
return ApiResponse.fail();
}
userService.save(user);
return ApiResponse.success(HttpStatus.OK.name(), null);
}
}
@ExtendWith(MockitoExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class AuthControllerTest {
@Mock
private User user;
@Autowired
private MockMvc mockMvc;
AutoCloseable openMocks;
ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
public void setup() {
openMocks = MockitoAnnotations.openMocks(this);
}
@Test
@DisplayName("로컬 회원 가입 테스트")
public void saveTest() throws Exception
{
// given
user = User.builder()
.userName("minchoi")
.passwd("1234")
.providerType(ProviderType.LOCAL)
.roleType(RoleType.USER)
.build();
// when & then
mockMvc.perform(MockMvcRequestBuilders
.post("/api/v1/auth/save")
.content(objectMapper.writeValueAsString(user))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
@ExtendWith(MockitoExtension.class)
: 이 애너테이션은 JUnit 5의 확장 모델을 사용하여 Mockito를 사용할 수 있도록 해줘요. MockitoExtension
은 Mockito에서 테스트 더미를 관리하고 주입하는 데 사용돼요.@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
: 이 애너테이션은 Spring Boot 애플리케이션의 전체 컨텍스트를 로드하여 통합 테스트를 실행할 때 사용해요. webEnvironment
속성은 테스트를 실행할 웹 환경을 지정하고, MOCK
로 설정하면 내장 서블릿 컨테이너 대신에 Mock Servlet 환경을 사용해요.@AutoConfigureMockMvc
: 이 애너테이션은 MockMvc를 자동으로 구성하여 Spring MVC 컨트롤러를 테스트할 수 있도록 해요. MockMvc를 사용하여 HTTP 요청을 보내고 응답을 검증할 수 있어요. MockMvc는 내부적으로 Spring의 DispatcherServlet을 사용하여 요청을 처리해요.@RestController
public class UserVehicleController {
private final UserVehicleService userVehicleService;
@Autowired
public UserVehicleController(UserVehicleService userVehicleService) {
this.userVehicleService = userVehicleService;
}
@GetMapping("/{username}/vehicle")
public ResponseEntity<String> getVehicleDetails(@PathVariable String username) {
// UserVehicleService를 사용하여 사용자의 차량 세부 정보를 가져옴
VehicleDetails vehicleDetails = userVehicleService.getVehicleDetails(username);
if (vehicleDetails != null) {
// 차량 세부 정보를 문자열로 반환
String vehicleInfo = vehicleDetails.getMake() + " " + vehicleDetails.getModel();
return ResponseEntity.ok(vehicleInfo);
} else {
// 사용자가 차량을 소유하고 있지 않을 경우 Not Found 반환
return ResponseEntity.notFound().build();
}
}
}
@WebMvcTest
를 이용한 단위 테스트@WebMvcTest(UserVehicleController.class)
public class UserVehicleControllerTests {
@Autowired
private MockMvc mvc;
@MockBean
private UserVehicleService userVehicleService;
@Test
public void testExample() throws Exception {
given(this.userVehicleService.getVehicleDetails("sboot"))
.willReturn(new VehicleDetails("Honda", "Civic"));
this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
.andExpect(status().isOk()).andExpect(content().string("Honda Civic"));
}
}
@WebMvcTest
역시 슬라이스 테스트를 지원하는 애너테이션 중 하나예요.@SpringBootTest
통합 테스트를 통한 진행이 이뤄진답니다.자바 코드 작성 패키지
테스트 코드 작성 패키지
EntityManager
@DataJpaTest
public class UserRepositoryTests {
//생략
@Test
public void testSaveUser() {
//given
User user = DummyUser.createDummyUser("testuser", "test@example.com");
//when
userRepository.save(user);
//then
assertNotNull(user.getId()); // ID가 생성되었는지 확인
}
}
given
, when
, then
으로 이루어져 있어요.given
when
Test slicing is about segmenting the ApplicationContext that is created for your test. Typically, if you want to test a controller using MockMvc, surely you don’t want to bother with the data layer. Instead you’d probably want to mock the service that your controller uses and validate that all the web-related interaction works as expected.
테스트 슬라이싱(Test slicing)은 테스트할 때 생성되는 ApplicationContext를 세분화하는 것입니다. 일반적으로 MockMvc를 사용하여 컨트롤러를 테스트하려는 경우 데이터 레이어에 신경 쓸 필요가 없습니다. 대신에 컨트롤러가 사용하는 서비스를 모킹하고, 모든 웹 관련 상호 작용이 예상대로 작동하는지 확인할 것입니다.
즉, 테스트 슬라이싱은 테스트를 세분화하여 필요한 부분만 포함하도록 합니다. 데이터 레이어, 서비스 레이어, 웹 레이어 등과 같은 다양한 레이어를 포함할 수 있습니다. 예를 들어, 컨트롤러를 테스트할 때 실제 데이터베이스에 액세스할 필요 없이 서비스 레이어만 모킹하여 웹 상호 작용을 테스트할 수 있습니다.
이를 통해 테스트를 더욱 효율적으로 작성하고, 필요한 부분만 집중적으로 테스트할 수 있습니다. 결과적으로 테스트 코드의 실행 속도가 향상되고 테스트 관리가 더욱 용이해집니다.
@SpringBootTest
어노테이션을 이용하면 모든 테스트를 할 수 있는데 왜 레이어별로 잘라서 테스트할까?
@SpringBootTest
어노테이션의 단점은 아래와 같아요.
Bean
을 로드하기 때문에 시간이 오래걸리고 무거워요.따라서 @SpringBootTest
어노테이션은 어플리케이션 컨텍스트 전체를 사용하는 통합 테스트에 사용돼야 합니다.
스프링 부트는 자동 설정의 일부만을 테스트 슬라이스로 가져와서 테스트에 활용할 수 있도록 다양한 테스트 어노테이션을 제공해줘요.
아래는 대표적인 슬라이스 테스트 어노테이션입니다.
@WebMvcTest
@WebFluxTest
@DataJpaTest
@JsonTest
@RestClientTest
@Controller
@ControllerAdvice
@JsonComponent
Converter
GenericConverter
Filter
WebMvcConfigurer
HandlerMethodArgumentResolver
@Service
, @Repository
)은 Bean
으로 등록하지 않아요.@WebMvcTest
를 사용함으로써 @Service Bean
이 등록되지 않았기 때문에, Controller
의 Service
에 대한 의존성이 깨져서 테스트를 진행할 수 없기 때문에 MockBean을 사용해서 서비스 빈을 등록해서 사용할 수 있게 해야 해given()
으로 행동을 예측한 후willReturn()
으로 해당 행동 예측 후의 반환 값을 설정this.userVehicleService.getVehicleDetails("sboot")
를 호출하여 서비스 레이어의 getVehicleDetails()
메서드가 정상적으로 호출되고, 반환되는 결과가 컨트롤러에 의해 올바르게 처리되는지를 검증하고 있어요.Bean
이 예상대로 동작하도록 하고 싶을 때, Mock Bean
을 사용하는 것이에요.Mock Bean
을 사용해요.슬라이스 테스트 시, 하위 레이어는 Mock
기반으로 만들기 때문에 주의할 점들이 있다.
Mock
을 사용한다면 내부 구현도 알아야 하고, 테스트 코드를 작성하며 테스트의 성공을 의도할 수 있기 때문에 완벽한 테스트라 보기 힘들다.그렇다면 언제 Mock
기반의 테스트를 사용해야 할까?
LocalDate.now()
처럼 계속 흘러가는 시간의 순간Bean
들을 사용해야 하는 통합 테스트가 아니라면, 슬라이스 테스트를 시도해보자.Mock
기반의 슬라이스 테스트라면 주의하여 엄격하게 사용해야 한다.MemberUpdateDTO updateMember(Long memberId, MemberUpdateDTO memberUpdateDTO);
//업데이트
@PostMapping(value = "/update/{id}")
public MemberUpdateDTO updateMember(@PathVariable("id") Long id, MemberUpdateDTO memberUpdateDTO){
return memberService.updateMember(id, memberUpdateDTO);
}
@DisplayName("회원수정 - 성공")
@Test
public void updateSuc() throws Exception {
Long updateMemberId = 1L;
Member dummyMember = Member.builder()
.name("name1")
.loginId("memberId1")
.password("password1")
.phoneNumber("01033333333")
.build();
MemberUpdateDTO memberUpdateDTO = MemberUpdateDTO.builder()
.name("update!!!")
.loginId("update!!!")
.password("update!!!")
.phoneNumber("update!!!")
.build();
given(this.memberService.updateMember(anyLong(), any(MemberUpdateDTO.class)))
.willReturn(memberUpdateDTO);
ObjectMapper mapper = new ObjectMapper();
this.mvc.perform(post("/members/update/{id}", updateMemberId)
.content(mapper.writeValueAsString(memberUpdateDTO))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("update!!!"))
.andExpect(jsonPath("$.loginId").value("update!!!"))
.andExpect(jsonPath("$.password").value("update!!!"))
.andExpect(jsonPath("$.phoneNumber").value("update!!!"))
.andDo(print());
}
perform(post("/members/update/{id}", updateMemberId)
: POST 요청으로 /member/update/{id} PathVariable 사용할 땐 위와 같이 진행해요.content(mapper.writeValueAsString(memberUpdateDTO))
: POST 요청으로 해당 DTO가 BODY를 통해서 넘어가야 하기 때문에 이를 위해서 ObjectMapper를 사용해요.willReturn(memberUpdateDTO)
실제 반환될 타입과 endExpect()에서 기대되는 부분을 맞춰줘야 해요.