Spring Boot에서 테스트 모듈은 spring-boot-test와 spring-boot-test-autoconfigure가 존재한다.
대부분의 경우 spring-boot-starter-test(테스트 스타터 패키지)만으로 충분하다.
개발자는 개발 사이클의 아무 시점(any point of their development cylce)에서 유닛 테스트를 실행하는 것을 꺼려서는 안된다. (유닛 테스트의 수가 수천개일지더라도…)
개발자는 테스트 실행 후 수 초 이내에 원하는 결과를 얻을 수 있어야 한다.
각각의 모든 유닛 테스트는 환경 변수나 설정에 대해서, 유닛 테스트는 그 결과가 모든 다른 요소에 의해 영항받지 않도록 다른 모든 요소로부터 독립적이어야 한다.
각 테스트는 서로 의존적이면 안된다. 한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안된다. 순서를 바꿔서 실행되도 괜찮아야 한다.
테스트는 반복 가능하고 결정론적(deterministic)이어야 한다. 다른 환경에서 실행될 때에도 값이 변경되지 않아야 한다.
각 테스트는 스스로의 데이터를 설정해야하고, 테스트 실행을 위해 다른 외부 요소에 의존해서는 안된다.
테스트 성공 여부를 수동으로 확인할 필요가 없어야 한다. 테스트는 스스로 성공여부를 판단할 수 있어야 한다.
테스트는 적시에 작성해야 한다. 유닛 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.
TDD(Test driven development) 방법론에 적합한 원칙이지만 실제로 지켜지지 않는 경우도 있다.
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)를 사용하지 않고 모킹한 코드이다.
단위 테스트로는 RequestMapping, Data Binding, Validation 등을 커버할 수 없다.
따라서 궁극적으로 테스트 커버리지를 높이기 위해서는 통합 테스트를 실시해야만 한다.
통합 테스트는 애플리케이션의 신뢰도를 높일 수 있다.
하지만 통합테스트는 느리다는 큰 단점을 가지고 있기 때문에, 통합 테스트를 적절하게 사용해야 한다.
@SpringBootTest 어노테이션을 사용하면 스프링이 실행되고, ApplicationContext를 생성하여 작동한다.
기본적으로 @RunWith(SpringRunner.class) 어노테이션과 함께 사용해야 한다.
별도의 클래스를 지정하지 않으면 전체 애플리케이션을 로드하고, 모든 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());
}
}
통합 테스트를 위해서는 굳이 필드 인젝션을 생성자 주입으로 바꿀 필요가 없다. 스프링이 알아서 주입해준다.
통합 테스트를 진행하며 빈 자체를 모킹해야하는 경우 사용한다.
@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{
//...
}
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/
안녕하세요 잘읽었습니다.
1. 그런데 @SpringBootTest를 붙였으면 @RunWith(SpringRunner.class)를 안붙여도 되지 않을까요?
2. MockBean 테스트에서, assertEquals("mocha", actualCoffee.getName());이 아니라 assertEquals("커피모카", actualCoffee.getName());가 되어야 하지 않을까요? 성공하는 테스트를 의도하신 거라면요.