TL;DR
@WebMvcTest
를 이용해 컨트롤러 단만 테스트하는 상황에서 @Service
는 Bean으로 등록이 되지 않기 때문에 대신 @AutoConfigureMockMvc
로 통합 테스트를 진행하거나 @MockBean
을 이용해 직접 ApplicationContext에 등록해야 한다.
Description:
Parameter 0 of constructor in com.api.restful.board.controller.ArticleController required a bean of type 'com.api.restful.board.service.ArticleService' that could not be found.
Action:
Consider defining a bean of type 'com.api.restful.board.service.ArticleService' in your configuration.
(중략)
java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'articleController' defined in file
(중략)
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.api.restful.board.service.ArticleService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
package com.api.restful.board.controller;
import java.net.URI;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
@Controller
@RequestMapping(value="/api/article", produces = MediaTypes.HAL_JSON_VALUE)
public class ArticleController {
@PostMapping
public ResponseEntity createArticle() {
URI createdUri = linkTo(methodOn(ArticleController.class).createArticle()).slash("{articleId}").toUri();
return ResponseEntity.created(createdUri).build();
}
}
package com.api.restful.board.service;
import java.time.LocalDateTime;
import java.util.Optional;
import org.springframework.stereotype.Service;
import com.api.restful.board.domain.Article;
import com.api.restful.board.repository.ArticleRepository;
@Service
public class ArticleService implements ServiceInterface{
private final ArticleRepository repository;
public ArticleService(ArticleRepository repository) {
this.repository = repository;
}
public Article save(Article article) {
// add regDateTime
LocalDateTime now = LocalDateTime.now();
article.setRegDateTime(now);
article.setLastUpdatedDateTime(now);
article.setHidden(false);
return repository.save(article);
}
package com.api.restful.board.controller;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
class ArticleControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void createArticle() throws Exception {
// 201 response == isCreated
mockMvc.perform(post("/api/article/")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaTypes.HAL_JSON) // Hypertext Application Language
)
.andExpect(status().isCreated());
}
}
스택오버플로우 및 국내 블로그 검색 결과, Service Class에 @Service 어노테이션을 제대로 붙이지 않아 발생하는 경우가 많다고 한다: @Service는 확실히 붙어있다.
ComponentScan에 해당 Service Class가 잡히지 않았을 수 있다: 같은 패키지 내의 다른 Service Class는 멀쩡하게 잘 잡혔다. 게다가 Application Class에 다음과 같이 스캔할 경로를 지정해주어도 문제가 해결되지 않았다.
@SpringBootApplication(scanBasePackages = {"com.api.restful.board.service", "com.api.restful.board"})
public class RestfulBoardApplication {
public static void main(String[] args) {
SpringApplication.run(RestfulBoardApplication.class, args);
}
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
}
테스트 클래스의 @WebMvcTest 어노테이션은 Web 레이어 관련 Bean들만 등록하기 때문에 Service가 등록되지 않아 발생한 문제이다. https://gracelove91.tistory.com/88
위 테스트코드들은 모두 통합테스트다. 기본적으로 @SpringBootTest 어노테이션을 사용하면 스프링이 관리하는 모든 빈을 등록시켜서 테스트하기 때문에 무겁다.
하지만 @WebMvcTest는 web 레이어 관련 빈들만 등록하므로 비교적 가볍다.
web레이어 관련 빈들만 등록되므로 Service는 등록되지않는다. 따라서 가짜로 만들어줄 필요가 있다 (@MockBean)
해결 1: Controller단만 별도로 Slice Test하는 대신 그냥 통합 테스트를 진행한다. @SpringBootTest 어노테이션 사용 시 MockMvc를 Bean으로 등록시키지 않기 때문에 @AutoConfigureMockMvc 를 추가해야 한다고 한다.
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
class ArticleControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void createArticle() throws Exception {
// test code here
}
해결 2: Service를 MockBean 어노테이션을 이용해 ApplicationContext에 등록한다
@RunWith(SpringRunner.class)
@WebMvcTest(ArticleController.class)
class ArticleControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
ArticleService articleService;
@Test
public void createArticle() throws Exception {
// test code here
}
P.S.
선생님이 작성한 테스트를 확인해보니 별도로 @MockBean을 통해 Service를 등록하지 않아도 @WebMvcTest가 잘 돌아가던데 대체 무슨 원리인지 모르겠다...