스프링 테스트(4) - MVC 컨트롤러에 대한 테스트

2024. 3. 1. 00:44spring/TEST

 

과제 : 컨트롤러 단위 테스트

스프링 MVC 프레임워크로 개발한 웹 애플리케이션의 컨트롤러 테스트하기

 

해결책

DispatcherServlet은 스프링 MVC 컨트롤러에 HTTP 요청/응답 객체를 전달하고 컨트롤러는 요청을 처리 후 뷰를 렌더랑하기 위해 다시 DispatcherServlet에 요청 객체를 반환한다.

 

출처 : https://howtodoinjava.com/spring-mvc/spring-dispatcherservlet-tutorial/

 

스프링 MVC 컨트롤러를 단위 테스트할 때 신경써야 할 부분은 HTTP 요청/응답 객체를 모방하는 것이다.

스프링은 서블릿 API용 목 객체 세트를 제공하여 컨트롤러 단위 테스트를 지원한다.

 

스프링 MVC 컨트롤러를 테스트하려면 DispatcherServlet에 올바른 객체가 반환됐는지 확인해야 한다. 스프링에서 기본 제공되는 각종 assertion 유틸리티를 이용해 객체의 content를 확인할 수 있다.

 

풀이

예시 : 은행 직원이 계정 번호화 예금액을 입력하는 웹 인터페이스를 개발하려고 한다.

@RequiredArgsConstructor
@Controller
public class DepositController {
    
    private final AccountService accountService;
    
    @GetMapping("/deposit")
    public String deposit(
            @RequestParam("accountNo") String accountNo,
            @RequestParam("amount") double amount,
            ModelMap model
    ) {
        accountService.deposit(accountNo, amount);
        model.addAttribute("accountNo", accountNo);
        model.addAttribute("balance", accountService.getBalance(accountNo));
        return "success";
    }
}

 

이 컨트롤러를 테스트해보자

 

class DepositControllerTest {

    private static final String TEST_ACCOUNT_NO = "1234";
    private static final double TEST_AMOUNT = 50;
    private AccountService accountService;
    private DepositController depositController;

    @BeforeEach
    void init() {
        accountService = mock(AccountService.class);
        depositController = new DepositController(accountService);
    }

    @Test
    void deposit() {
        //when
        when(accountService.getBalance(TEST_ACCOUNT_NO)).thenReturn(150.0);
        ModelMap model = new ModelMap();

        //then
        String viewName =
                depositController.deposit(TEST_ACCOUNT_NO, TEST_AMOUNT, model);

        assertEquals(viewName, "success");
        assertEquals(model.get("accountNo"), TEST_ACCOUNT_NO);
        assertEquals(model.get("balance"), 150.0);
    }
}

 

 

 

과제 : 컨트롤러 통합 테스트

 

스프링 애플리케이션을 통합 테스트하려면 애플리케이션 컨텍스트에 선언된 빈을 가져와야 한다. 스프링 테스트 지원 기능 없이 작성하면 Junit이 제공하는 @Before나 @BeforeCalss를 붙인 테스트 초기화 메서드에서 애플리케이션 컨텍스트를 직접 수동으로 로드해야 한다. 하지만 빈 개수가 많으면 동일한 애플리케이션 컨텍스트가 여러 번 로드될 수 있다.

 

해결책

 

스프링 테스트 지원 기능을 활용하면 여러 빈 구성 파일로부터 애플리케이션 컨텍스트를 자겨오거나, 여러 테스트를 걸쳐 컨텍스트를 캐시하는 등 테스트용 애플리케이션 컨텍스트를 관리하는데 용이하다. 또한 애플리케이션 컨텍스트는 단일 JVM 안의 모든 테스트에 걸쳐 구성 파일 위치를 키로 하여 캐시된다.

 

기본 테스트 실행 리스너

테스트 실행 리스너 설명
DependencyInjectionTestExecutionListener 애플리케이션 컨텍스트를 비롯한 모든 의존체를 테스트에 주입한다.
DirtiesContextTextExecutionListener,
DirtiesContextBeforeModesTestExecutionListener
@DirtiesContext 처리를 담당하며 필요 시 애플리케이션 컨텍스트를 다시 로드한다.
TransactionalTestExecutionListener 테스트 케이스의 @Transactional을 처리하며 테스트 끝에 롤백 한다.
sqlScriptsTestExecutionListener @Sql을 붙인 테스트를 감지해서 테스트를 시작하기 전에 주어진 SQL을 실행한다.
ServletTestExecutionListener @WebAppConfiguration이 발견되면 웹 애플리케이션 컨텍스트를 로드한다.

 

테스트 컨텍스트 프레임워크에서 애플리케이션 컨텍스트를 관리하려면 내부적으로 테스트 클래스와 테스트 컨텍스트 관리자를 연계해야 한다. 이런 용도로 테스트 컨텍스트 프레임워크에는 몇가지 지원 클래스가 있다. JUnit은 AbstractJunit4SpringContextTests 클래스인데 ApplicationContextAware 인터페이스를 구현하며 테스트 컨텍스트 관리자와 연계되므로 protected 필드인 applicationContext를 사용해 애플리케이션 컨텍스트를 가져올 수 있다.

 

테스트 프레임워크에 맞는 테스트 컨텍스트 지원 클래스를 상속해 테스트 클래스를 작성하면 된다.

 

JUnit을 사용하면 테스트 컨텍스트 지원 클래스를 상속하지 않고도 테스트 클래스를 테스트 컨텍스트 관리자에 연계하고 직접 ApplicationContextAware 인터페이스를 구현할 수 있다.

 

풀이

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
class AccountServiceJunitContextTest implements ApplicationContextAware {

    private static final String TEST_ACCOUNT_NO = "1234";
    private ApplicationContext applicationContext;
    private AccountService accountService;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @BeforeEach
    void init() {
        accountService = applicationContext.getBean(AccountService.class);
        accountService.createAccount(TEST_ACCOUNT_NO);
        accountService.deposit(TEST_ACCOUNT_NO, 100);
    }

    @Test
    void deposit() {
        accountService.deposit(TEST_ACCOUNT_NO, 50);
        assertEquals(accountService.getBalance(TEST_ACCOUNT_NO), 150, 0);
    }
    
    @Test
    void withDraw() {
        accountService.withdraw(TEST_ACCOUNT_NO, 50);
        assertEquals(accountService.getBalance(TEST_ACCOUNT_NO), 50, 0);
    }
}

 

사실 @Autowire로 구현체를 만들지 않고 빈을 주입 받는게 일반적이다.

 

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
class AccountServiceJunitContextTest {

    private static final String TEST_ACCOUNT_NO = "1234";
 
    @Autowired
    private AccountService accountService;


    @BeforeEach
    void init() {
        accountService.createAccount(TEST_ACCOUNT_NO);
        accountService.deposit(TEST_ACCOUNT_NO, 100);
    }

    @Test
    void deposit() {
        accountService.deposit(TEST_ACCOUNT_NO, 50);
        assertEquals(accountService.getBalance(TEST_ACCOUNT_NO), 150, 0);
    }
    
    @Test
    void withDraw() {
        accountService.withdraw(TEST_ACCOUNT_NO, 50);
        assertEquals(accountService.getBalance(TEST_ACCOUNT_NO), 50, 0);
    }
}

 

자동연결 후보가 여럿일 경우에는 @Qualifier에 이름을 적어 구체적으로 명시해도 되고 @Resource를 붙여 이름으로 빈을 찾아 자동 연결해도 된다.