Spring Boot

[Spring Boot] JUnit을 활용한 테스트 코드 작성(1)

byeongoo 2020. 5. 3. 21:29

1. 테스트 코드 작성 이유

테스트 코드를 작성하는 이유는 여러가지가 있습니다. 코드의 안정성을 높이고, 기능을 추가하거나 수정하면서 발생하는 부작용(Side-Effect)를 줄일 수 있습니다. 그렇기 때문에 기존 코드를 수정하는 것에 대한 불안감을 줄일 수 있고, 결과적으로 생산성을 높여줄 수 있습니다. 또한 더 깔끔하고 좋은 코드를 작성하게 해줍니다.

2. F.I.R.S.T 단위 테스트 원칙

단위 테스트는 가장 작은 단위의 테스트입니다. 단위 테스트만 구성되어도 굉장히 많은 문제를 해결할 수 있습니다. 

  • Fast : 테스트 코드를 실행하는 일은 오래 걸리면 안된다.
  • Independent:독립적으로 실행이 되어야 한다.
  • Repeatable : 테스트는 실행할 때마다 같은 결과를 만들어야 한다.
  • Self-validating : 메뉴얼 없이 테스트 코드만 실행해도 성공, 실패 여부를 알 수 있어야 한다.
  • Timely : 유닛 테스트는 프로덕션 코드가 테스트를 성공하기 직전에 구성되어야 한다. 즉, 바로 사용가능해야 한다.

3. 스프링부트 테스트 디펜던시

스프링부트는 애플리케이션 테스트를 위한 많은 기능을 제공합니다. 크게 두 가지 모듈을 제공합니다.

  • spring-boot-test : 핵심 기능 포함
  • spring-boot-test-autoconfigure : 테스트를 위한 AutoConfiguration 제공

스프링부트 테스트 관련 애노테이션으로는 다음과 같은 것들이 있습니다.

어노테이션 설명 Bean
@SpringBootTest 통한 테스트 Bean 전체
@WevMvcTest 단위테스트, MVC 테스트 MVC 관련된 Bean
@DataJpaTest 단위 테스트, JPA 테스트 JPA관련 Bean
@RestClientTest 단위 테스트, Rest API 테스트 일부 Bean
@JsonTest 단위 테스트, Json 테스트 일부 Bean

@SpringBootTest

통합 테스트를 위해 제공하는 기본적인 스프릉부트 테스트 어노테이션입니다. 여러 단위 테스트를 하나의 통합된 테스트로 수행할 때 적합합니다. 실제 구동되는 애플리케이션과 똑같이 컨텍스트를 로드하여 모든 Bean을 등록하고 테스트를 하기 때문에 애플리케이션의 규모가 클수록 느려집니다. JUnit의 SpringJUnit4ClassRunner 클래스를 상속 받는 @RunWith(SpringRynnver.class)를 꼭 붙여서 사용해야 정상동작합니다.

4. 스프링부트 단위 테스트

스프링부트 환경에서 단위 테스트를 작성해보겠습니다. @SpringBootTest 어노테이션을 사용하지 않고, 즉, 스프링을 전혀 실행하지 않는다는 얘기입니다. @SpringBootTest는 스프링을 실행시켜야하기 때문에 단위 테스트가 아니라고 보지만 다른 책에서는 단위 테스트라고 언급하기도 합니다. 

 

아래와 같은 Repository와 Service 객체가 있습니다.

@Repository
public class CoffeRepository {
    
    private Map<String, Coffee> coffeeMap = new HashMap<>();
    
    public Coffee findByName(String name){
        return coffeeMap.get(name);
    }
    
    public void add(Coffee coffee){
        coffeeMap.put(coffee.getName(), coffee);
    }
    
}
@Service
public class CoffeeService {
    
    private final CoffeeRepository coffeeRepository;
    
    public CoffeeService(CoffeeRepository coffeeRepository){
        this.coffeeRepository = coffeeRepository;
    }
    
}

이제 해당 코드를 테스트하는 코드를 작성해보겠습니다. 커피 이름으로 조회를 잘 하는지 테스트하는 코드입니다. @Before에서 테스트전에 coffeeRepository를 만들어서 세팅을해주고 coffeeService객체에 주입을 해줍니다.

public class CoffeeServiceUnitTest{
    
    private CoffeeRepository coffeeRepository;
    
    private CoffeeService coffeeService;
    
    @Before
    pulic void setUp(){
        coffeeRepository = new CoffeeRepository();
        coffeeRepository.add(new Coffee("mocha"));
        coffeeService = new CoffeeService(coffeeRepository);
    }
    
    @Test
    public void getCoffeeTest(){
        Coffee coffee = coffeeService.findByName("mocha");
        assertEquals("mocha", coffee.getName());
    }
    
}

단위 테스트만 잘 작성하여도 많은 버그를 잡을 수 잇습니다. 하지만 부득이하게 통합테스트를 진행해야하는 상황도 있습니다. 모든 코드를 단위테스트로 수행하겠다는 것은 바람직하지 않습니다. 하지만 스프링 통합 테스트는 너무 느리다는 단점이 있습니다.

5. @SpringBootTest와 @MockBean을 이용한 테스트

@SpringBootTest 어노테이션을 사용하여 통합 테스트를 진행하면서 Mock 객체를 사용해야 하는 경우가 있습니다. 스프링에서는 @MockBean 어노테이션을 제공합니다. "mocha"라는 이름으로 findByName 메소드를 실행하면 "커피모카"라는 이름을 가진 Coffee 객체를 반환해줍니다. 그리고 assertThat을 이용하여 2개의 값이 동일한지 테스트 코드를 작성합니다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class CoffeeServiceUnitTest{
    
    @Autowired
    private CoffeeRepository coffeeRepository;
    
    @MockBean
    private CoffeeService coffeeService;
    
    @Before
    pulic void setUp(){
        Coffee coffee = coffeeService.findByName("mocha");
        when(coffeeRepository.findByName("mocha").thenReturn(new Coffee("커피모카"));
    }
    
    @Test
    public void getCoffeeTest(){
        Coffee coffee = coffeeService.findByName("mocha");
        assertThat("커피모카", coffee.getName());
    }
    
}

@SpringBootTest를 이용할 경우 이 테스트와 전혀 상관없는 Bean을 모두 생성합니다. 그래서 테스트 수행 시간이 너무 오래 걸립니다. 통합 테스트를 수행하면서 특정 클래스만 테스트할 수 있도록 @SpringBootTest에 클래스를 지정해줍니다.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {
        CoffeeService.class,
        CoffeeRepository.class
})
public class CoffeeServiceUnitTest{
    
    @Autowired
    private CoffeeRepository coffeeRepository;
    
    @MockBean
    private CoffeeService coffeeService;
    
    @Before
    pulic void setUp(){
        Coffee coffee = coffeeService.findByName("mocha");
        when(coffeeRepository.findByName("mocha").thenReturn(new Coffee("커피모카"));
    }
    
    @Test
    public void getCoffeeTest(){
        Coffee coffee = coffeeService.findByName("mocha");
        assertThat("커피모카", coffee.getName());
    }
    
}

이렇게 설정할 경우 테스트를 수행하면 CoffeeService와 CoffeeRepository 두 개의 Bean만 생성을 합니다. 전체 애플리케이션을 실행하는 방법보다 훨씬 빠르게 가능합니다.

6. 실무에서의 @SpringBooTest

실무에서는 @SpringBootTest 어노테이션을 너무 많이 사용하고 있습니다. 단위 테스트를 해도 되는 상황이 이 애노테이션을 사용하는 것은 바람직하지 않습니다. 통합테스트라면 @SpringBootTest 어노테이션을 사용하고, 그렇지 않다면 @SpringBootTest 어노테이션을 사용하면 안됩니다. 통합테스트 중에서도 특정 클래스만하면 되는 경우에는 @SpringBootTest 어노테이션의 classes 속성에 반드시 클래스를 지정해주도록합니다.

 

통합테스트와 단위테스트의 비중을 고르는건 매우 어려운일입니다. 보통 통합테스트 50%, 단위테스트 50%로 반반씩 구성을 한다는 의견이 많습니다. 각자의 상황에 맞게 결정해야합니다.

REFERENCE

https://medium.com/@ssowonny/%EC%84%A4%EB%A7%88-%EC%95%84%EC%A7%81%EB%8F%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%9E%91%EC%84%B1-%EC%95%88-%ED%95%98%EC%8B%9C%EB%82%98%EC%9A%94-b54ec61ef91a

 

설마 아직도 테스트 코드를 작성 안 하시나요?

가끔 욕을 먹고 싶을 때가 있을 수도 있죠. 가끔 지탄을 받고 꾸중을 들음으로써 자극을 받고 정신을 차리고 싶을 수도 있습니다. 아니면 혹은 그냥 아무 이유 없이 갑자기 한심한 눈초리를 받고 싶을 때가 있을 수도 있겠죠. 그럴 땐 주변에 있는…

medium.com

https://brunch.co.kr/@springboot/207

 

스프링부트 테스트(1)

스프링 부트 Unit Test 및 Integration Test | 테스트코드를 작성하는 일은 정말 중요하다. 하지만, 필자에게 아직도 너무 어려운 일이 바로 테스트 코드를 작성하는 일이다. TDD 를 잘하는 개발자는 필��

brunch.co.kr

https://cheese10yun.github.io/spring-boot-test/

 

Spring Boot Test - Yun Blog | 기술 블로그

Spring Boot Test - Yun Blog | 기술 블로그

cheese10yun.github.io