Spring Boot testing

지명근·2021년 7월 19일
0

전공공부

목록 보기
9/9

1. Spring Boot의 테스트 모듈

Spring Boot에서 테스트 모듈은 spring-boot-test와 spring-boot-test-autoconfigure가 존재한다.
대부분의 경우 spring-boot-starter-test(테스트 스타터 패키지)만으로 충분하다.

포함된 주요 라이브러리

  • JUnit
  • Spring Test & Spring Boot Test
  • AssertJ
  • Hamcrest
  • Mockito (기본 버전은 1.x이지만, 2.x로 변경 가능)
  • JSONassert
  • JsonPath

2. Unit 테스트의 FIRST 원칙

Fast, 빠르게 실행되어야 한다.

개발자는 개발 사이클의 아무 시점(any point of their development cylce)에서 유닛 테스트를 실행하는 것을 꺼려서는 안된다. (유닛 테스트의 수가 수천개일지더라도…)

개발자는 테스트 실행 후 수 초 이내에 원하는 결과를 얻을 수 있어야 한다.

Independent(Isolated), 독립적으로 실행되어야 한다.

각각의 모든 유닛 테스트는 환경 변수나 설정에 대해서, 유닛 테스트는 그 결과가 모든 다른 요소에 의해 영항받지 않도록 다른 모든 요소로부터 독립적이어야 한다.

각 테스트는 서로 의존적이면 안된다. 한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안된다. 순서를 바꿔서 실행되도 괜찮아야 한다.

Repeatable, 반복 가능해야 한다.

테스트는 반복 가능하고 결정론적(deterministic)이어야 한다. 다른 환경에서 실행될 때에도 값이 변경되지 않아야 한다.

각 테스트는 스스로의 데이터를 설정해야하고, 테스트 실행을 위해 다른 외부 요소에 의존해서는 안된다.

Self Validating, 스스로 검증가능해야 한다.

테스트 성공 여부를 수동으로 확인할 필요가 없어야 한다. 테스트는 스스로 성공여부를 판단할 수 있어야 한다.

Timely(Thorough), 바로 사용 가능해야 한다.

테스트는 적시에 작성해야 한다. 유닛 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.

TDD(Test driven development) 방법론에 적합한 원칙이지만 실제로 지켜지지 않는 경우도 있다.

3. Mockito를 활용한 모킹

spring-boot-starter-test에는 Mockito가 포함되어 있으므로, Mockito를 사용하는 예제를 참고하였다.
아래 유닛 테스트를 예제에서는 스프링에서 제공하는 @SpringBootTest 어노테이션을 사용하지 않는다.

이는 스프링 그 자체에도 의존성을 갖지 않겠다는 의미이다.

@SpringBootTest는 스프링을 실행시켜야 하기 때문에, 관점에 따라 단위 테스트가 아닐 수 있다고 한다.
참고한 게시글에서는 일단 스프링을 전혀 실행하지 않는 테스트인 경우만 단위 테스트로 정의하고, 아래 예제 코드들을 제시했다.

// CoffeeRepository.java
public interface CoffeeRepository{
  Coffee findByName(String name);
}

// SimpleCoffeeRepository.java
@Repository
public class SimpleCoffeeRepository implements CoffeeRepository{
  private Map<String, Coffee> coffeeMap = new HashMap<>();
  
  @Override
  public Coffee findByName(String name){
    return coffeeMap.get(name);
  }
}

// CoffeeService.java
@Service
public class CoffeeService{
  @Autowired
  private CoffeeRepository coffeeRepository;    // 필드 인젝션
}

여기서 Service를 보면 @Autowired로 레포지토리를 주입받고 있는데, 스프링 실행 없이는 빈을 주입 받을 수 없다. 그래서 스프링 없이 유닛 테스트를 할 수 없다.

따라서 생성자 주입 방식으로 변경한다.

// CoffeeService.java
@Service
public class CoffeeService{
  private final CoffeeRepository coffeeRepository;
  
  public CoffeeService(CoffeeRepository coffeeRepository){
    this.coffeeRepository = coffeeRepository;   // 생성자 주입
  }
}

여담으로 인텔리J에서도 필드 인젝션을 쓰면 경고가 뜬다.
왜냐하면 필드 주입은 Setter를 통한 주입과 유사한 방식으로 이루어지기 때문에, Setter를 통한 주입의 단점을 그대로 따라가기 때문이다.

Setter를 통한 주입은 필요한 객체가 주입되지 않아도 얼마든지 객체를 생성할 수 있는 문제가 있다.

그러므로 생성자 주입을 쓰도록 하자. @Autowired를 쓰는 경우 생성자 위에 추가하면 된다.

더 알아보기

예시 코드 : 커피 조회 서비스

public class CoffeeServiceUnitTest{
  @Test
  public void findByNameTest(){
    // Given
    CoffeeRepository repo = Mockito.mock(CoffeeRepository.class);
    Mockito.when(repo.findByName("mocha"))
      .thenReturn(new Coffee("mocha"));
    CoffeeService coffeeService = new CoffeeService(repo);    
    
    // When
    Coffee actualCoffee = coffeeService.findByName("mocha");
    
    // Then
    assertEquals("mocha", actualCoffee.getName());
  }
}

Mockito를 사용해서 DB(위 코드에서는 HashMap)를 사용하지 않고 모킹한 코드이다.

  • 6번째 라인에서 Mockito에 findByName 사용 시 어떤 결과값을 받을지 설정을 하였고,
  • 11번째 라인에서 findByName이 호출되면 Mockito가 이를 인터셉트하여 설정한 결과값을 반환한다.
  • 14번째 라인에서는 실제로 DB에서 가져온 값과 내가 추측하는 값이 일치한지 비교를 한다.

4. Integration Test

단위 테스트로는 RequestMapping, Data Binding, Validation 등을 커버할 수 없다.
따라서 궁극적으로 테스트 커버리지를 높이기 위해서는 통합 테스트를 실시해야만 한다.

통합 테스트는 애플리케이션의 신뢰도를 높일 수 있다.
하지만 통합테스트는 느리다는 큰 단점을 가지고 있기 때문에, 통합 테스트를 적절하게 사용해야 한다.

장점

  • 모든 빈을 컨테이너에 올리고 테스트하기 때문에, 운영환경과 유사한 환경에서 테스트가 가능.
  • 전체적인 테스트를 진행할 수 있어 코드 커버리지가 높아짐.

단점

  • 모든 빈을 컨테이너에 올려야 하기 때문에 느리다.
  • 전체적인 테스트를 한번에 하기 때문에, 특정 계층, 특정 빈에서 발생하는 오류의 디버깅이 어려움.

5. Spring Boot의 Integration Test

@SpringBootTest 어노테이션을 사용하면 스프링이 실행되고, ApplicationContext를 생성하여 작동한다.

기본적으로 @RunWith(SpringRunner.class) 어노테이션과 함께 사용해야 한다.

@SpringBootTest

별도의 클래스를 지정하지 않으면 전체 애플리케이션을 로드하고, 모든 Bean을 생성한다.

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

  @Autowired
  private CoffeeService coffeeService;

  @Test
  public void findByNameTest(){    
    Coffee actualCoffee = coffeeService.findByName("mocha");
    assertEquals("mocha", actualCoffee.getName());
  }
}

통합 테스트를 위해서는 굳이 필드 인젝션을 생성자 주입으로 바꿀 필요가 없다. 스프링이 알아서 주입해준다.

@MockBean

통합 테스트를 진행하며 빈 자체를 모킹해야하는 경우 사용한다.

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

  @Autowired
  private CoffeeService coffeeService;
  
  @MockBean
  private SimpleCoffeeRepository simpleCoffeeRepository   // 모킹
  
  @Before                                                 // 테스트를 하기 전에 초기화
  public void initialization(){
    given(this.simpleCoffeeRepository.findByName("mocha"))  // 모킹 상황 설정
      .willReturn(new Coffee("커피모카"));                   // 반환값 설정
  }

  @Test
  public void findByNameTest(){    
    Coffee actualCoffee = coffeeService.findByName("mocha");
    assertEquals("mocha", actualCoffee.getName());
  }
}

한계

@SpringBootTest 어노테이션은 클래스를 지정하지 않는다면, 기본적으로 모든 컴포넌트의 Bean을 생성한다.

때문에 테스트를 수행 시간이 너무 오래 걸린다.

아래 코드와 같이 특정 클래스를 지정하면 원하는 클래스만 테스트해 볼 수 있다.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {
    CoffeeService.class,
    SimpleCoffeeRepository.class
  })
public class MockBeanTest{
  //...
}

Reference

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing
https://brunch.co.kr/@springboot/207
https://meetup.toast.com/posts/124
https://medium.com/@tasdikrahman/f-i-r-s-t-principles-of-testing-1a497acda8d6
https://nesoy.github.io/articles/2018-02/CleanCode-UnitTest
https://tech.buzzvil.com/handbook/test-principles/

profile
백엔드 지향

1개의 댓글

comment-user-thumbnail
2021년 10월 13일

안녕하세요 잘읽었습니다.
1. 그런데 @SpringBootTest를 붙였으면 @RunWith(SpringRunner.class)를 안붙여도 되지 않을까요?
2. MockBean 테스트에서, assertEquals("mocha", actualCoffee.getName());이 아니라 assertEquals("커피모카", actualCoffee.getName());가 되어야 하지 않을까요? 성공하는 테스트를 의도하신 거라면요.

답글 달기