WebMvcTest를 이용한 스프링부트 테스트에서 Consider defining a bean of type (...) in your configuration 오류

bestKimEver·2022년 12월 29일
0
post-custom-banner

TL;DR
@WebMvcTest 를 이용해 컨트롤러 단만 테스트하는 상황에서 @Service는 Bean으로 등록이 되지 않기 때문에 대신 @AutoConfigureMockMvc로 통합 테스트를 진행하거나 @MockBean을 이용해 직접 ApplicationContext에 등록해야 한다.


상황

  • 백기선 선생님의 RESTful API 강좌를 들으며, 들은 내용을 활용해 게시판 API를 만들던 중 Controller Slice Test에서 오류 발생
  • 오류 메시지를 읽어보니 Controller(ArticleController) 생성 시 필요한 서비스(ArticleService)가 제대로 주입되지 않아 문제가 발생한 것 같다.

오류 메시지

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: {}

관련 코드

  • Controller(ArticleController)
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();
    }
}
  • Service(ArticleService)
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);
    }
  • Controller Test
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());                
    }
}

시도해본(그러나 실패한) 것들

  1. 스택오버플로우 및 국내 블로그 검색 결과, Service Class에 @Service 어노테이션을 제대로 붙이지 않아 발생하는 경우가 많다고 한다: @Service는 확실히 붙어있다.

  2. 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가 잘 돌아가던데 대체 무슨 원리인지 모르겠다...

profile
이제 3년차 개발새발자. 제가 보려고 정리해놓는 글이기 때문에 다소 미흡한 내용이 많습니다.
post-custom-banner

0개의 댓글