Spring

Spring Boot Test

JeonInwoo 2021. 2. 9. 16:20

스프링부트 테스트 에 대하여 알아보자

참고블로그 brunch.co.kr/@springboot/207

참고 이전포스팅 hodolee246.tistory.com/60


Spring Boot Test Dependency

  1. spring-boot-test : 핵심 기능 포함

  2. spring-boot-test-autoconfigure : 테스트를 위한 AutoConfiguration 제공

  • Spring Test, Spring Boot Test : 스프링 부트를 위한 유틸 및 통합 테스트 지원

  • AssertJ : 유창한 Assertion library

  • Hamcrest : libarary에 잘 어울리는 오브젝트(constraints or predicates)

  • Mockito : 자바 mocking framework

  • JSONassert : JSON을 위한 assertion library

  • JsonPath : JSON을 위한 XPath

테스트 결과의 일관성

별도 DB 서버에 문제가 있거나 외부 상태적 예외의 경우 일부 테스트가 실패할 수 있지만 그렇지 않은경우 동일한 테스트를 시도했을때 테스트의 결과가 성공하도 실패하기도 한다면 이는 좋은 테스트라 할 수 없다.

이처럼 테스트는 외부의 문제가 발생하지 않는이상 계속해서 일관적인 결과를 제공해주어야 한다.

JUnit프레임워크

자바로 단위 테스트를 지원해주는 도구이자 프레임워크이다.

프레임워크(의 동작 원리는 IoC이다)는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아서 주도적으로 애플리케이션의 흐름을 제어한다. 따라서 프레임워크에서 동작하는 코드는 main() 메소드도 필요 없고 오브젝트를 만들어서 실행시키는 코드를 만들 필요도 없다.

 

요구 조건

1. public으로 선언(JUnit4의 경우 명시를 해주어야함)

2. return이 void

3. 파라메터 0개

4. @Test 어노테이션 적용

 

실행순서

1. @Test 메소드 검색(public, void, none parameter)

2. 테스트 클래스의 오브젝트 하나 생성

3. @BeforeEach가 붙은 메소드가 실행

4. @Test 메소드 실행

5. @After가 붙은 메소드가 있으면 실행

6. 나머지 테스트 메소드에 대하여 반복

7. 모든 테스트의 결과를 종합하여 반환

 

JUnit5 지원 여부

스프링부트 2.20 이상은 기본 디펜던시로 JUnit5를 사용(2.20 이전의 버전에서도 JUnit5를 사용할 수 있지만 공식적인 방법은 아니다.)

테스트 준비

// repository
public interface BeanRepository {
    Bean findByName(String name);
    void add(Bean bean);
}

@Repository
public class SimpleBeanRepository implements BeanRepository {

    private Map<String, Bean> beanMap = new HashMap<>();

    @Override
    public Bean findByName(String name) {
        return beanMap.get(name);
    }

    @Override
    public void add(Bean bean) {
        beanMap.put(bean.getName(), bean);
    }
}

// service
@Service
public class BeanService {

    private final BeanRepository beanRepository;

    public BeanService(BeanRepository beanRepository) {
        this.beanRepository = beanRepository;
    }

    public Bean findByName(String name) {
        return beanRepository.findByName(name);
    }
}

 

현재 생성자 주입을 통해 DI를 주입받는데 이는 스프링 의존을 줄이기 위한 DI 주입이다.

 -> 스프링 의존 없이 테스트 가능

@Autowired를 이용한 필드 주입을 받게 될 경우 스프링에 의존하는 DI주입이다.

 -> 스프링 의존 없이 테스트 불가능

 

테스트

public class BeanServiceUnitTest {

    private SimpleBeanRepository simpleBeanRepository;

    private BeanService beanService;

    @Before
    public void setUp() {
        simpleBeanRepository = new SimpleBeanRepository();
        simpleBeanRepository.add(new Bean("coffeeBean"));
        beanService = new BeanService(simpleBeanRepository);
    }

    @Test
    public void getBeanByName() {
        Bean bean = beanService.findByName("coffeeBean");
        assertEquals("coffeeBean", bean.getName());
    }
}

@SpringBootTest 을 안썻기에 스프링 의존없이 테스트를 진행

Mock Test

public class BeanServiceUnitMockTest {

    @Test
    public void getBeanByName() {
        // setup
        BeanRepository beanRepository = Mockito.mock(BeanRepository.class);
        // beanRepo 의 findByName 이 호출되면 새로운 bean 을 리턴
        Mockito.when(beanRepository.findByName("coffeeBean"))
                .thenReturn(new Bean("coffeeBean"));
        BeanService beanService = new BeanService(beanRepository);
        // when
        Bean actualBean = beanService.findByName("coffeeBean");
        // then
        assertEquals("coffeeBean", actualBean.getName());
    }
}

Mockito를 사용해서 가짜 객체를 사용할 경우 해당 메소드 실행 시 Mockito에 선언된 값으로 인터셉트 되어 실제 Repo로직이 실행되지 않는다.

Mockito를 사용하여 단위테스트를 성공하였지만, 우리는 현재 스프링을 사용하여 애플리케이션을 개발하고 있다. 그렇기에 스프링을 사용한 통합테스트를 하지않는다면 신뢰성이 떨어질 수 있다고 한다.

단위 및 통합테스트

스프링을 의존하여 테스트를 한 경우 통합테스트

의존하지 않고 테스트를 한 경우 단위테스트

 -> 하지만 이러한 기준은 명확한 기준이 아님

 -> 토비에서는 아래의 기준으로 설명함

1. 한 가지 관심에 집중할 수 있는 작은 단위의 테스트다.

2. 크기가 어느정도인지 정확하게 정해진건 아니다.

3. 일반적으로 단위는 작을수록 좋다.

4. 매번 DB의 상태가 달라지고, 테스트를 위해 DB를 특정 상태로 만들 수 없다면 단위 테스트로서 가치가 없어진다.

  • @SpringBootTest 어노테이션

    • 쉽게 스프링 어플리케이션 컨텍스르를 생성하여 사용이 가능하다. 단 사용 시 기본적으로 @RunWith(SpringRunner.class)를 함께 사용한다.(JUnit4한정)

  • @RunWith

    • @SpringBootTest 어노테이션을 사용하기 위해서 반드시 사용해야 한다.

      • Junit5에는 모든 @...Test 어노테이션에 @RunWith 상위 버전인 @ExtendWith가 붙어있다.

 

@SpringBootTest

@SpringBootTest 어노테이션을 사용하면 필요한 모든 Bean을 모두올려서 테스트를 할 수 있다.

->별도 클래스를 지정하지 않을경우 전체 애플리케이션을 로드하고 모든 Bean을 생성한다. 

++ 그 밖에 @SpringBootTest 어노테이션은 기본적으로 서블릿 서버를 실행하지 않는다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class BeanIntegrationTest {

    @Autowired
    private BeanService beanService;

    @Test
    public void getBeanByName() {
        Bean bean = beanService.findByName("coffeeBean");
        assertEquals("coffeeBean", bean.getName());
    }
}

내 Repo에는 아무런 데이터가 없기에 테스트는 실패한다.

@SpringBootTest 어노테이션을 이용한 통합테스트를 진행하는 방법은 코드로 이렇게 되며 쉽게 컴포넌트를 주입할 수 있다. 통합테스트를 할때에는 스프링을 사용하기에 컨스트럭트 인젝션이 아닌 필드 인젝션을 하더라도 스프링이 알아서 처리를 해준다.

@SpringBootTest, @MockBean

@RunWith(SpringRunner.class)
@SpringBootTest
public class BeanServiceTest {

    @Autowired
    private BeanService beanService;

    @MockBean
    private SimpleBeanRepository simpleBeanRepository;

    @Before
    public void addBean() {
        BDDMockito.given(this.simpleBeanRepository.findByName("coffeeBean"))
                .willReturn(new Bean("anyBean"));
    }

    @Test
    public void getBeanByName() {
        Bean bean = beanService.findByName("coffeeBean");
        assertEquals("anyBean", bean.getName());
    }
}

@SpringBootTest 어노테이션을 사용하여 통합 테스트를 진행하면서 Mock객체를 사용해야 하는 경우가 존재한다고 한다. 그렇기에 스프링에서는 @MockBean 어노테이션을 제공해준다. (해당 코드는 Repo를 목 객체로 사용하는 방법이다. + @SpringBootTest를 사용하여 Spring이 가동됨으로 콘솔창에 스프링 가동표시가 뜬다.)

@SpringBootTest 문제

@Repository
public class delayRepository {
    public delayRepository() {
        try {
            Thread.sleep(10000);	// 빈들이 엄청 많을 경우를 가정
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

모든 클래스를 컨텍스트에 올리게 될 경우 사용하지 않는 빈까지 올라가 많은 시간이 걸릴 수 있다.

 -> delayRepository : 딜레이 10초 : 수많은 빈이 있다 가정

이처럼 스프링 프레임워크의 기본으로 선언된 Bean과 개발자에 의하여 선언된 모든 Bean들이 쌓여 엄청난 시간이 걸릴 수 도 있게 된다.

해결방법

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {
                BeanService.class,
                SimpleBeanRepository.class
        })
public class BeanServiceTest {

    @Autowired
    private BeanService beanService;

    @MockBean
    private SimpleBeanRepository simpleBeanRepository;

    @Before
    public void addBean() {
        BDDMockito.given(this.simpleBeanRepository.findByName("coffeeBean"))
                .willReturn(new Bean("anyBean"));
    }

    @Test
    public void getBeanByName() {
        Bean bean = beanService.findByName("coffeeBean");
        assertEquals("anyBean", bean.getName());
    }
}

위에서 말한것처럼 @SpringBootTest에 클래스를 지정하는 것이다. @SpringBootTest를 이용하여 특정 클래스 혹은 컴포넌트를 테스트하고 싶을경우 classes 속성을 이용하여 클래스를 직접 선택할 수 있다.

 -> 하지만 @SpringBootTest 어노테이션으로 인하여 스프링의존하여 테스트를 진행하기 때문에 단위테스트라 볼 수 없을 수 있다.

결론

스프링을 사용할지 말지는 개발자에게 달려있으므로 잘 판단하여 테스트를 진행하자

 -> 신뢰성을 위하여 스프링을 사용하여 테스트할 실행시간이 길어질 수 있다.

    -> 그렇기에 delayRepo 예제처럼 많은 빈들이 올라가 오래 걸리는 테스트일 경우 사용하는 빈만 올려서 테스트를 진행하자.