미운 오리 새끼의 우아한 개발자되기

[Spring Boot] Cloud Native Java - Test (1) 모의 기법 활용 본문

Spring & Spring Boot/Spring Boot

[Spring Boot] Cloud Native Java - Test (1) 모의 기법 활용

Serina_Heo 2022. 9. 27. 10:28

모의 기법 활용

스프링부트는 @MockBean 어노테이션을 지원한다. @MockBean 은 Application Context 에 있는 Bean 을 대신하는 모의 Mockito Bean 을 만들고, Application Context 에 있는 원래 Bean 의 동작을 끄라고 Spring 에게 지시한다. 

@RunWith(SpringRunner.class) 만 붙어있는 Test 클래스에는 @SpringBootTest 어노테이션이 없으므로 테스트가 실행될 때 Application Context 가 로딩되지 않는다. 따라서 이 테스트는 통합테스트가 아닌 단위테스트다. 이 클래스에서 테스트 되는 컴포넌트는 AccountService Bean 이고 AccountService Bean 은 UserService Bean 을 통해 외부 마이크로서비스와 협력한다. 

-> Application Context 의 로딩 유무에 따라 통합테스트이거나 단위테스트로 나뉘어진다.

package demo.account;

import demo.user.User;
import demo.user.UserService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Collections;
import java.util.List;

import static org.assertj.core.api.Assertions.assetThat;
import static org.mockito.BDDMockito.given;

@RunWith(SpringRunner.class)
public class AccountServiceTests {

    // UserService를 대신하는 모키토 모의 객체를 생성
    @MockBean
    private UserService userService;
    
    @MockBean
    private AccountRepository accountRepository;
    
    private AccountService accountService;
    
    // 모의 객체를 파라미터로 전달하고 새로운 AccountService 를 생성한다
    @Before
    public void before() {
    	accountService = new AccountService(accountRepository, userService);
    }
    
    @Test
    public void getUserAccountReturnsSingleString() throws Exception {
        // findAccountByUsername(String username)를 Stub해서 findAccountByUsername("user")가 호출되면 지정한 값이 반환되게 한다
    	given(this.accountRepository.findAccountByUsername("user"))
        	.willReturn(
            	Collections.singletonList(
                	new Account("user", new AccountNumber("123456789"))));
        
        // getAuthenticatedUser() 를 Stub 해서 지정한 값이 반환되게 한다
        given(this.userService.getAuthenticatedUser())
        	.willReturn(new User(0L, "user", "John", "Doe"));
        
        // 모의 객체를 인자로 받아서 생성된 AccountService 객체의 getUserAccounts() 메소드를 호출한다
        List<Account> actual = accountService.getUserAccounts();
        
        assertThat(actual).size().isEqualTo(1);
        assertThat(actual.get(0).getUsername()).isEqualTo("user");
        assertThat(actual.get(0).getAccountNumber()).isEqualTo(new AccountNumber("123456789"));

이 테스트는 Application Context 를 로딩하지 않았으므로 단위테스트이며, UserService Bean 안에 HTTP를 통해 원격의 지원 서비스를 호출하지 않고도 AccountService 의 기능을 테스트할 수 있었다. AccountRepository 컴포넌트도 마찬가지로 실제 외부의 지원 서비스를 호출하지 않았지만 AccountService 기능을 테스트할 수 있었다. AccountRepository는 관계형 DB에 매핑되는 Account 엔티티드를 관리하는 역할을 하며, 자동 설정에 의해 Application Context 에 로딩된다. 우리가 원하는 것은 AccountService 의 기능을 테스트하는 것이므로, AccountRepository 컴포넌트의 모의 인스턴스를 만들고 테스트 목적에 맞게 동작하도록 만들면 테스트의 복잡도를 낮출 수 있다.

package demo.account;

import demo.user.UserService;
import org.springramework.beans.factory.annotation.Autowired;
import org.springframework.streotype.Service;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

@Service
public class AccountService {
   
    private final AccountRepository accountRepository;

    // UserService 는 생성자를 통해 주입된다
    private final UserService userService;

    // 생성자 파라미터에 해당하는 빈은 Application Context 로부터 생성자에 주입된다
    @Autowired
    public AccountService(AccountRepository ar, UserService us) {
       this.accountRepository = ar;
       this.userService = us;
    }

    public List<Account> getUserAccount() {
        // getAuthenticatedUser() 메소드는 HTTP로 원격의 사용자 마이크로서비스를 호출한다
        return Optional.ofNullable(userService.getAUthenticatedUser())
        .map(u -> accountRepository.findAccountByUsername(u.getUsername()))
        .orElse(Collections.emptyList());
    }
}

항상 필드를 통한 의존관계 주입 대신 생성자를 통한 의존관계 주입을 사용하자. 필드를 통한 의존관계 주입을 사용할 수도 있지만, 그렇게 하면 객체 하나를 생성하기 위해 필수 조건으로 어떤 컴포넌트가 필요한지 알기 어렵다. 일반적인 관점에서 생성자를 통한 의존관계 주입이 더 타당하지만 테스트 관점에서는 완전히 새로운 차원의 이야기가 된다. 

package demo.user;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.streotype.Service;
import org.springframework.web.client.RestTemplate;

import java.net.URI;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.RequestEntity.get;

@Service
public class UserService {
    
    private final Stirng serviceHost;
    private final RestTemplate restTemplate;
    
    @Autowired
    public UserService(RestTemplate restTemplate, @Value("${user-service.host:user-service}")String sh) {
        this.serviceHost = sh;
        this.restTemplate = restTemplate;
    }
    
    public User getAuthenticatedUser() {
        URI url = URI.create(String.format("http://%s/uaa/v1/me", serviceHost));
        RequestEntity<Void> request = get(url).header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE).build();
        return restTemplate.exchange(request, User.class).getBody();
    }
}

마이크로서비스에서는 HTTP로 원격 서비스와 통합해야 하는 많은 협력 컴포넌트에 의존하는 스프링 MVC 컨트롤러를 통합 테스트하는 일이 매우 흔하다. @MockBean은 이런 통합 테스트에서 특히 제 역할을 톡톡히 해낸다. 스프링 MVC 컨트롤러를 테스트하려면 웹 환경이 필요하므로 웹 환경을 구성할 수 있는 Application Context 도 필요하다. 이 테스트 시나리오에서는 외부 애플리케이션을 호출하는 서비스에 대해서만 모의 객체를 만들면 된다. 웹 애플리케이션의 경계에 있는 모의 객체를 통해 테스트 대상 컴포넌트의 원격 웹 서비스에 대한 의존관계를 제거하고 격리할 수 있다. 이렇게 하면 JVM의 경계를 벗어날 필요없이 애플리케이션 내부의 모듈만으로도 통합테스트를 수행할 수 있다.

 

[Reference] Cloud Native Java - Josh Long, Kenny Bastani, 책만