이번 포스팅에서는 @SpringBootTest 애노테이션을 활용한 통합 테스트와 @WebMvcTest를 사용해서 컨트롤러 계층에 집중한 테스트 이 두 가지 스프링부트 테스트를 진행할 것이다.
우선 예제에 사용할 코드를 설정한다.
Product.java
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Long id;
private String name;
private int price;
private int stockQuantity;
@Enumerated(value = EnumType.STRING)
private SellingStatus sellingStatus;
@Builder
private Product(String name, int price, int stockQuantity, SellingStatus sellingStatus) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
this.sellingStatus = sellingStatus;
}
public boolean isEnoughStockQuantity(int quantity) {
return stockQuantity >= quantity;
}
public void deductQuantity(int quantity) {
if (!isEnoughStockQuantity(quantity)) {
throw new IllegalArgumentException("재고 수량이 부족합니다.");
}
this.stockQuantity -= quantity;
}
public void editProductInfo(String name, int price, int stockQuantity, SellingStatus sellingStatus) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
this.sellingStatus = sellingStatus;
}
public void addStockQuantity(int quantity) {
this.stockQuantity += quantity;
}
}
SellingStatus.java
@Getter
public enum SellingStatus {
SELLING("판매중"),
HOLD("판매보류"),
STOP_SELLING("판매종료")
;
SellingStatus(String desc) {
this.desc = desc;
}
private final String desc;
public static boolean isSelling(SellingStatus sellingStatus) {
return SELLING == sellingStatus;
}
}
테스트를 진행하기 위해서 우리는 Junit5와 AssertJ를 사용할 것이다.
Junit5
XUnit는 켄트 벡이 단위 테스트를 위해 테스트 프레임워크를 만든것이 발전하며 만들어졌다.
Junit은 JVM에서 테스트를 하기위한 테스트 프레임워크이다.
The de-facto standard for unit testing Java applications.
스프링 부트에서는 자바 어플리케이션을 테스트하기위한 사실상의 표준 프레임워크로 소개한다.
AssertJ
테스트를 위해 사용하는 라이브러리로, 다양한 기능 및 오류 메시지와 테스트 코드 가독성을 향상시켜준다. 그리고 메서드 체이닝을 지원한다.
@SpringBootTest
스프링 부트에서 제공하는 테스트를 위한 애노테이션으로, 통합 테스트를 위해 사용된다. 스프링 부트 애플리케이션을 실행하는 데 필요한 모든 Context와 Bean들을 모두 로드해서 테스트 환경을 구성한다.
@SpringBootTest 애노테이션에 사용되는 속성은 다음과 같은 것들이 있다.
classes
: 애플리케이션 컨텍스트를 로드할 때 사용할 구성(설정) 클래스를 지정한다. ex)classes = {CustomConfig.class}
properties
: 웹 환경에서 설정할 property값을 설정한다. ex)properties = {"my.name=kang"}
webEnvironment
: 웹 환경을 위한 설정으로 기본 값은 MOCK이며 모의(가짜) 웹 환경을 사용한다.
테스트는 우리가 보편적으로 사용하는 MVC 구조에서 Persistence(Repository), Business(Service), Presentation(Controller) Layer를 각각 테스트 진행할 것이다.
Persistence Layer(Repository)
Repository를 테스트하는 이유는 MyBatis를 사용하면 Mapper 파일에 CRUD 쿼리부터 다양한 쿼리를 만들어서 사용하게 되고, JPA를 사용하더라도 기본 제공 CRUD 메서드 외에 JPQL이나 Native Query를 작성하게된다.
커스텀으로 작성한 쿼리들이 원하는대로 동작하는지 확인할 필요가 있으므로 테스트를 작성한다.
ProductRepository.java
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findAllBySellingStatusIn(List<SellingStatus> sellingStatus);
}
여기서는 JPA Query Method로 생성된 쿼리가 의도대로 동작하는지 테스트할 것이다.
- Product를 4개 생성하고 DB에 저장한다.
- DB에서 Product의 상태가 SELLING(판매중)인 상태의 Product만 조회한다.
then절에서 테스트 결과를 검증할 때 두 가지 방식을 사용했는데 첫 번째 방식은 리스트의 인덱스를 조회해서 테스트하는 방법이고, 두 번째 방법은 전체 리스트를 한 번에 테스트하는 방법이다. 주로 두 번째 방법을 이용한다.
AssertJ는 메서드 체이닝을 지원하기 떄문에 두 번째 방식으로 편하게 테스트결과를 검증할 수 있다. 객체를 검증할 때 주로 사용하는 메서드들을 확인해볼 것이다. contains()
메서드는 비슷한 메서드들이 많아서 주로 사용되는 3가지 메서드를 설명한다.
extracting()
: 검증하는 객체의 원하는 필드만 추출해서 테스트할 수 있다.contains()
: extracting으로 추출한 필드가 객체에 포함되어있는지 확인한다. contains는 포함여부만 확인하기 때문에 순서나 리스트의 사이즈를 넘는 값들을 튜플로 설정해도 에러가 나지 않는다.containsExactly()
: 추출한 필드가 포함되어 있는지 확인한다. 검증하는 객체나 리스트의 크기만큼 튜플을 입력해야 하고 순서가 정확해야 한다.containsExactlyInAnyOrder()
: 추출한 필드가 포함되어 있는지 확인한다. 검증하는 객체나 리스트의 크기만큼 튜플을 입력해야 하고 순서는 상관하지 않는다.
테스트를 실행하고 결과를 확인한다.
Business Layer(Service)
우선 테스트에 사용할 서비스 클래스를 구현한다.
예제를 간소화하기 위해서 리포지토리에서 테스트한 쿼리 메서드를 Response 객체로 변환해서 리턴하는 로직이다.
테스트클래스에 Persistance 계층 테스트와 달리 @Transactional
애노테이션을 사용했는데 이는 하나의 테스트가 끝나고 데이터를 커밋하지 않고 롤백하기 위함이다.
테스트 시 @Transactional
애노테이션을 사용하게 되면 Service 클래스에 @Transactional
애노테이션을 적용하지 않아도 테스트가 통과하게 될 수 있다.
이렇게 부가적으로 발생할 수 있는 문제들을 인지한 후 애노테이션을 사용하는 것을 권장한다.
다른 방법으로는 tearDown()
절에서 직접 테스트에서 등록된 데이터를 하나하나 삭제해주는 방법이 있다.
비즈니스 로직을 테스트하는데 현재 코드는 리포지토리 계층과 코드의 차이가 크게 없다.
메서드의 파리미터만 객체로 request 객체를 생성해서 전달하고 응답을 엔티티로 받는 것이 아닌 response 객체로 변환해서 받는 것이라는 차이가 있다.
테스트 검증은 Persistance 레이어에서 테스트한 방법과 동일하다. 테스트가 통과한 것을 확인할 수 있다.
@WebMvcTest
@WebMvcTest
는 Spring MVC의 컨틀롤러를 테스트하기 위해서 사용한다.
이 애노테이션은 @SpringBootTest
와 같이 모든 애플리케이션 컨텍스트와 Bean이 로드되는 것이 아닌 @Controller
, @ControllerAdvice
, @JsonComponent
, Converter
, GenericConverter
, Filter
, HandlerInterceptor
, WebMvcConfigurer
, WebMvcRegistrations
, HandlerMethodArgumentResolver
와 같은 MVC 동작에 필요한 빈들로 제한한다.
@WebMvcTest
애노테이션을 활용해서 우리는 원하는 컨트롤러 클래스를 지정해서 테스트를 할 수 있다.
Persentation Layer(Controller)
Persistance 레이어를 테스트할 때는 상품을 등록하는 코드를 예제로 사용할 것이다.
컨트롤러에서는 보통 파라미터로 넘어오는 프로퍼티들의 검증을 담당한다. 이 때 @WebMvcTest에서는 서비스 계층의 빈들을 자동으로 구성하지도 않을 뿐더러 우리는 서비스 계층의 동작은 관심이 없다.
이 때 SpringBoot의 @MockBean
을 사용해서 가짜 객체를 주입함으로써 테스트환경을 구성할 수 있다.
@WebMvcTest
를 적용하고 테스트할 컨트롤러 클래스를 지정해준다.- 이 애노테이션이을 사용하면 자동으로 구성되는
MockMvc
객체를 사용할 수 있다. Mock 객체는 가상(가짜) MVC 환경을 구성해준다.
@MockBean에 대해서 설명하면, 지금 테스트하고자 하는 로직은 컨트롤러 계층이다. 이 애노테이션은 스프링 컨테이너에 Mockito 라이브러리가 제공하는 가짜 객체를 만들어서 넣어준다.
우리는 실제 동작하는 ProductService객체가 아닌 가짜 객체를 넣어줌으로써, 현재 테스트하고자하는 컨트롤러 계층만 테스트할 수 있다.
- 상품을 생성하는 메서드의 파라미터로 들어올 Request 객체를 설정한다. 다음으로 mockMvc를 설정해준다.
perform()
: 수행할 요청에 대한 설정을 한다. 로직에서는 post요청의 url, Content-Type, 파리미터로 받을 JSON 데이터를 설정했다.andExpect()
: 요청을 수행했을 때 결과값과 기대값을 비교해서 검증을 수행한다.andDo()
: 요청에 대한 결과를 print로 콘솔에 출력할 수 있다.
요청 객체에 대해서 모든 파라미터를 정상적으로 입력했을 때 원하는 대로 status가 200으로 나타나는 것을 확인할 수 있다.
다음으로는 파라미터의 값을 validation에 위배되게 입력했을 때의 결과를 확인한다.
위에서 ProductCreateRequest
에 Validation을 적용한 것과 같이 상품명을 넣지 않았을 때 BindException
에 걸려서 HttpStatus는 BadRequest가 나오고 상태코드는 400으로 결과가 나타날 것이다.
테스트 코드에서 사용한 jsonPath는 JsonPath를 참조 바란다.
테스트는 정상적으로 통과한 것을 확인할 수 있다.
지금까지 @SpringBootTest
, @WebMvcTest
를 사용해서 Repository, Service, Controller를 테스트 했다.
그런데 컨트롤러 계층을 테스트할 때 처럼 @MockBean
을 사용해서 서비스 계층도 가짜 객체를 통해서 그 계층만 테스트할 수 있을 것 같다.
다음번에는 스프링부트가 제공하는 Mockito 라이브러리를 사용해서 서비스 계층의 단위 테스트를 진행해볼 것이다.
참조
- https://docs.spring.io/spring-boot/docs/3.1.11/reference/htmlsingle/#features.testing
- https://assertj.github.io/doc/
'테스트코드' 카테고리의 다른 글
테스트 코드 작성의 필요성 (2) | 2024.04.21 |
---|