Spring boot TDD 및 JaCoCo 테스트 커버리지
1, Given – When – Then
2, F.I.R.S.T
Given – When – Then
Given 테스트에 필요한 환경을 설정하는 단계, 필요한 변수, Mock 객체로 특정 상황의 행동 정의
※ Mock 객체 -테스트 작성을 위해 사용하는 거짓된 객체, 실제 객체가 아님
When 목적을 보여주는 단계, 테스트 코드를 포함하여 테스트하고 결과 값을 가짐
Then 결과를 검증하는 단계, 이 단계에서 결과 값을 검증, 결과 값이 아니어도 테스트를 하여 검증한 것이 있으면 같이 검증
F.I.R.S.T
빠르게(Fast)
테스트는 빠르게 수행돼야 한다. 테스트가 느리면 코드를 개선하는 작업이 느려져 코드 품질이 떨어질 수 있다
테스트 속도에 절대적인 기준은 없지만 목적을 단순하게 설정해서 작성하거나 외부 환경을 사용하지 않는 단위 테스트를 작성하는 것 등을 빠른 테스트라고 할 수 있다.
고립된, 독립적(Isolated)
하나의 테스트 코드는 목적으로 여기는 하나의 대상에 대해서 만 수행되어야 한다
만약 하나의 테스트가 다른 테스트 코드와 상호작용 하거나 관리할 수 없는 외부 소스를 사용하게 되면 외부 요인으로 인해 테스트가 수행되지 않을 수 있다
반복 가능한(Repeatable)
테스트는 어떤 환경에서도 반복 가능하도록 작성해야 한다 이 의미는 앞의 Isolated 규칙과 비슷한 의미를 갖고 있다
테스트는 개발 환경의 변화나 네트워크의 연결 여부와 상관없이 수행되어야 한다
자가 검증(Self-Validating)
테스트는 그 자체 만으로도 테스트 검증이 완료돼야 한다 테스트가 성공했는지 실패했는지 확인할 수 있는 코드를 함께 작성해야 한다
만약 결과값과 기댓값을 비교하는 작업을 코드가 아니라 개발자가 직접 확인하고 있다면 좋지 못한 테스트 코드다
적시에(Timely)
테스트 코드는 테스트하려는 애플리케이션 코드를 구현하기 전에 완성되어야 한다
너무 늦게 작성된 테스트 코드는 정상적인 역할을 수행하기 어려울 수 있다
또한 테스트 코드로 인해 발견된 문제를 해결하기 위해 소모되는 개발 비용도 커지기 쉽다
스프링 부트의 테스트 설정
테스트 환경이 쉽도록 지원, pom.xml에 의존성을 추가한다
<dependencies>
... 생략 ...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
... 생략 ...
</dependencies>
라이브러리 추가 시 이러한 의존성을 가지게 된다
test 라이브러리는 다양한 테스트 도구를 지원, 자동으로 설정을 지원한다
JUnit 5 : 자바 애플리케이션의 단위 테스트를 지원한다
Spring Test & Spring Boot Test : 스프링 부트 애플리케이션에 대한 유틸리티와 통합 테스트를 지원
AssertJ : 다양한 단정문(assert)을 지원하는 라이브러리
Hamcrest : Matcher를 지원하는 라이브러리이다.
Mockito : 자바 Mock 객체를 지원하는 프레임 워크
JSONassert : JSON용 단정문 라이브러리이다
JsonPath : JSON용 XPath를 지원한다
JUnit의 생명 주기
생명 주기와 관련된 어노테이션
@Test : 테스트 코드를 포함한 메서드를 정의한다
@BeforeAll : 테스트를 시작하기 전에 호출되는 메서드를 정의한다
@BeforeEach : 각 테스트 메거드가 실행되기 전에 동작하는 메서드를 정의한다
@AfterAll : 테스트를 종요하면서 호출되는 메서드를 정의한다
@AfterEach : 각 테스트 메서드가 종료되면서 호출되는 메서드를 정의한다
동작 알아보기
com.springboot.test - TestLifeCycle.java 파일 생성
package com.springboot.test;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class TestLifeCycle {
@BeforeAll
static void beforeAll() {
System.out.println("## BeforeAll Annotation 호출 ##");
System.out.println();
}
@AfterAll
static void afterAll() {
System.out.println("## afterAll Annotation 호출 ##");
System.out.println();
}
@BeforeEach
void beforeEach() {
System.out.println("## beforeEach Annotation 호출 ##");
System.out.println();
}
@AfterEach
void afterEach() {
System.out.println("## afterEach Annotation 호출 ##");
System.out.println();
}
@Test
void test1() {
System.out.println("## test1 시작 ##");
System.out.println();
}
@Test
@DisplayName("Test Case 2!!!")
void test2() {
System.out.println("## test2 시작 ##");
System.out.println();
}
@Test
@Disabled
void test3() {
System.out.println("## test3 시작 ##");
System.out.println();
}
}
이것을 실행하면 나오는 콘솔 로그가 어떻게 출력 되는지 확인하고 특이한 점 확인
컨트롤러 객체의 테스트
컨트롤러란?
클라이언트에서 요청을 받음 → 서비스 컴포넌트로 요청을 전달 → 결과 값을 가공하여 응답
여러 레이어중 웹에 가장 가까히 있는 모듈
ProductController 를 대상으로 getProduct() 와 createProduct() 메서드에 대한 테스트 코드를 작성
ProductController - getProduct()
@RestController
@RequestMapping("/product")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService){
this.productService = productService;
}
@GetMapping()
public ResponseEntity<ProductResponseDto> getProduct(Long number){
ProductResponseDto productResponseDto = productService.getProduct(number);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
}
--- 생략 ---
}
ProductService의 객체를 의존성 주입받음
그런데 테스트 하는 입장에서 ProductController 만 테스트를 하고 싶다?
해결법 = Mock 객체 활용
test/java/com.springboot.test 패키지에 controllerr 생성, ProductControllerTest.java 파일 생성
package com.springboot.test.controller;
import com.springboot.test.data.dto.ProductResponseDto;
import com.springboot.test.service.impl.ProductServiceImpl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
ProductServiceImpl productService;
// <http://localhost:8080/api/v1/product-api/product/{productId}> 테스트 해볼것
@Test
@DisplayName("MockMvc를 통한 Product 데이터 가져오기 테스트")
void getProductTest() throws Exception {
given(productService.getProduct(123L)).willReturn(
new ProductResponseDto(123L, "pen", 5000, 2000));
String productId = "123";
mockMvc.perform(
get("/product?number=" + productId))
.andExpect(status().isOk())
.andExpect(jsonPath(
"$.number").exists())
.andExpect(jsonPath("$.name").exists())
.andExpect(jsonPath("$.price").exists())
.andExpect(jsonPath("$.stock").exists())
.andDo(print());
verify(productService).getProduct(123L);
}
@WebMvcTest(테스트 대상 클래스.class)
웹에서 사용되는 요청과 응답에 대한 테스트를 수행할 수 있다. 대상 클래스만 로드해 테스트를 수행,
만약 대상 클래스를 추가하지 않으면 @Controller, @RestController, @ContollerAdvice 등의 컨트롤러 관련 빈 객체가 모두 로드된다
@SpringBootTest보다 가볍게 테스트하기 위해 사용
@MockBean
MockBean은 실제 빈 객체가 아닌 Mock(가짜) 객체를 생성해서 주입하는 역할을 수행한다
MockBean 이 선언된 객체는 실제 객체가 아니기 때문에 실제 행위를 수행하지 않는다
그렇기 때문에 해당 객체는 개발자가 Mockito의 given() 메서드를 통해 동작을 정의해야 한다
@Test
테스트 코드가 포함돼 있다고 선언하는 어노테이션, JUnit Jupiter 에서는 이 어노테이션을 감지해서 테스트 계획이 포함시킨다
@DisplayName
테스트 메서드의 이름이 복작해서 가독성이 떨어질 경우 이 어노테이션을 통해 테스트에 대한 표현을 정의할 수 있다
일반적으로 @WebMvcTest 어노테이션을 사용한 테스트는 슬라이스 테스트(Slice test) 라고한다
레이어 별로 나누어서 테스트를 하는데, 컨트롤러는 Web과 맞닿아 있는 레이어 이기 때문에
외부요인을 차단하고 테스트하면 의미가 없다
ProductController에 작성되어 있는 createProduct() 메서드 구현
@RestController
@RequestMapping("/product")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService){
this.productService = productService;
}
@PostMapping()
public ResponseEntity<ProductResponseDto> createProduct(@RequestBody ProductDto productDto){
ProductResponseDto productResponseDto = productService.saveProduct(productDto);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
}
}
createProduct() 메서드는 @RequestBody로 값을 받고 있다.
이에 따른 테스트 코드
사용된 ProductDto의 코드
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ProductDto {
private String name;
private int price;
private int stock;
}
이렇게 하고서 실행하면 코드가 실행이 안된다
이유: Gson에 대한 의존성이 없기 때문
Gson = JSON 파생된 라이브러리 (구글 개발)
JSON 문자열로 변환하거나 JSON 문자열을 자바 객체로 변환
그러므로 pom.xml 파일에 의존성을 추가한다
<dependencies>
.. 내용 생략 ..
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
.. 내용 생략 ..
</dependencies>
서비스 객체의 테스트
DAO의 역할이 드러나지 않기 때문에 DAO는 생략
getProduct() 메서드의 테스트 코드 작성
test/java/com.springboot.test - service/impl 패키지를 생성하고 ProductServiceTest.java 파일을 생성한다
package com.springboot.test.service.impl;
import com.springboot.test.data.dto.ProductResponseDto;
import com.springboot.test.data.entity.Product;
import com.springboot.test.data.repository.ProductRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.Optional;
import static org.mockito.Mockito.verify;
public class ProductServiceTest {
private ProductRepository productRepository = Mockito.mock(ProductRepository.class);
private ProductServiceImpl productService;
@BeforeEach
public void setUpTest() {
productService = new ProductServiceImpl(productRepository);
}
@Test
void getProductTest() {
// given
Product givenProduct = new Product();
givenProduct.setNumber(123L);
givenProduct.setName("펜");
givenProduct.setPrice(1000);
givenProduct.setStock(1234);
Mockito.when(productRepository.findById(123L))
.thenReturn(Optional.of(givenProduct));
// when
ProductResponseDto productResponseDto = productService.getProduct(123L);
// then
Assertions.assertEquals(productResponseDto.getNumber(), givenProduct.getNumber());
Assertions.assertEquals(productResponseDto.getName(), givenProduct.getName());
Assertions.assertEquals(productResponseDto.getPrice(), givenProduct.getPrice());
Assertions.assertEquals(productResponseDto.getStock(), givenProduct.getStock());
verify(productRepository).findById(123L);
}
}
단위테스트를 위해서 외부요인을 모두 제거 → WebMvcTest 등의 어노테이션 x
이어서 saveProduce()의 단위 테스트
package com.springboot.test.service.impl;
import static org.mockito.AdditionalAnswers.returnsFirstArg; //추가
import static org.mockito.ArgumentMatchers.any; //추가
import static org.mockito.Mockito.verify;
import com.springboot.test.data.dto.ProductDto; //추가
import com.springboot.test.data.dto.ProductResponseDto;
import com.springboot.test.data.entity.Product;
import com.springboot.test.data.repository.ProductRepository;
import java.util.Optional;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
public class ProductServiceTest {
private ProductRepository productRepository = Mockito.mock(ProductRepository.class);
private ProductServiceImpl productService;
@BeforeEach
public void setUpTest() {
productService = new ProductServiceImpl(productRepository);
}
@Test
void saveProductTest() {
// given
Mockito.when(productRepository.save(any(Product.class)))
.then(returnsFirstArg());
// when
ProductResponseDto productResponseDto = productService.saveProduct(
new ProductDto("펜", 1000, 1234));
// then
Assertions.assertEquals(productResponseDto.getName(), "펜");
Assertions.assertEquals(productResponseDto.getPrice(), 1000);
Assertions.assertEquals(productResponseDto.getStock(), 1234);
verify(productRepository).save(any());
}
}
중요한것 - save(any(Product.class))
any()→ Mock객체의 동작 정의 or 검증하는 과정에서 특정한 매개변수를 전달하지 않고 매서드의 실행만 확인
MockBean 어노테이션을 사용한 테스트 환경 설정
... 위에있는 import ...
@ExtendWith(SpringExtension.class)
@Import({ProductServiceImpl.class})
class ProductServiceTest2 {
@MockBean
ProductRepository productRepository;
@Autowired
ProductService productService;
... test 코드 ...
기존- Mockito로 Mock객체를 SpringBean에 등록x, 객체를 직접 초기화 하여 사용
MockBean- 스프링에 Mock 객체를 등록하여 주입 받는 형식
리포지트리 객체의 테스트
개발자가 구현하는 레이어 중 DB와 가깝다
리포지토리 객체의 테스트 코드를 작성할 때 고려할점
1, findById(), save() 같은 기본 메서드에 대한 테스트는 큰 의미가 없다 -> 리포지토리의 기본 메서드는 테스트 검증을 마치고 제공된 것이기 때문
2, 데이터베이스의 연동 여부는 테스트 시 고려해 볼 사항이다 굳이 따지면 데이터베이스는 외부 요인에 속한다 만약 단위 테스트를 고려한다면 데이터베이스를 제외할 수 있고, 테스트 용으로 다른 데이터베이스를 사용하는 경우도 있다 -> 데이터베이스를 사용한 테스트는 테스트 과정에서 데이터베이스에 테스트 데이터가 적재되기 때문 ※적재된 데이터를 그냥 둘 수 없으므로 제거하는 코드까지 작성
3, 데이터베이스를 제외한 테스트 상황을 가정해서 테스트 데이터베이스로 H2 DB 를 사용, 테스트 할때는 마리아DB를 사용 JpaRepository에서 제공하는 기본 메서드 사용
H2 DB를 사용한 테스트 코드를 작성
pom.xml 파일에 의존성을 추가
<dependencies>
...생략...
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
...생략...
</dependencies>
데이터베이스에 값을 저장하는 테스트 코드 작성
test/com.springboot.test -> data/repository 패키지를 생성 -> ProductRepositoryTestByH2.java 파일을 생성
package com.springboot.test.data.repository;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.springboot.test.data.entity.Product;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@DataJpaTest
public class ProductRepositoryTestByH2 {
@Autowired
private ProductRepository productRepository;
@Test
void saveTest() {
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(1000);
Product savedProduct = productRepository.save(product);
assertEquals(product.getName(), savedProduct.getName());
assertEquals(product.getPrice(), savedProduct.getPrice());
assertEquals(product.getStock(), savedProduct.getStock());
}
}
※@DataJpaTest 어노테이션을 사용하고 있다
@DataJpaTest는 다음과 같은 기능을 제공
1, JPA와 관련된 설정만 로드해서 테스트를 진행 기본적으로 @Transactional 어노테이션을 포함하고 있어 테스트 코드가 종료되면 자동으로 데이터베이스의 롤백이 진행된다
2, 기본 값으로 임베디드 데이터베이스를 사용
다른 데이터베이스를 사용하라면 별도의 설정을 거쳐 사용 가능하다
이후 정상적인 테스트가 이뤄졌는지 체크
save() 메서드의 리턴 객체와 Given에서 생성한 엔티티 객체의 값이 일치하는지 assertEquals() 메서드를 통해 검증
package com.springboot.test.data.repository;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.springboot.test.data.entity.Product;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
//@TestPropertySource("classpath:application-test.properties")
@DataJpaTest
public class ProductRepositoryTestByH2 {
@Autowired
private ProductRepository productRepository;
@Test
void selectTest() {
// given
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(1000);
Product savedProduct = productRepository.saveAndFlush(product);
// when
Product foundProduct = productRepository.findById(savedProduct.getNumber()).get();
// then
assertEquals(product.getName(), foundProduct.getName());
assertEquals(product.getPrice(), foundProduct.getPrice());
assertEquals(product.getStock(), foundProduct.getStock());
}
}
마리아DB에서 테스트하기 위해서는 별도의 설정이 필요
같은 패키지 경로
test/com.springboot.test -> data/repository 패키지 - ProductRepositoryTest.java 파일을 생성하고 아래와 같이 코드를 작성
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Test
void save() {
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(1000);
Product savedProduct = productRepository.save(product);
assertEquals(product.getName(), savedProduct.getName());
assertEquals(product.getPrice(), savedProduct.getPrice());
assertEquals(product.getStock(), savedProduct.getStock());
}
}
replace 요소는 @AutoConfigureTestDatabase 어노테이션의 값을 조정하는 작업을 수행한다
replace 속성의 기본값은 Replcae.ANY 이며, 이 경우 임베디드 메모리 데이터베이스를 사용한다
이 속성값을 Replace.NONE 으로 변경하면 애플리케이션에서 실제로 사용하는 데이터베이스로 테스트 가능
여기까지 하면 @DataJpaTest를 사용하지 않고 @SpringBootTest 어노테이션 으로도 테스트 가능
같은 패키지 경로에 ProductRepositoryTest2.java 파일을 생성
@SpringBootTest
public class ProductRepositoryTest2
{
@Autowired
ProductRepository productRepository;
@Test
public void basicCRUDTest() {
/* create */
// given
Product givenProduct = Product.builder()
.name("노트")
.price(1000)
.stock(500)
.build();
// when
Product savedProduct = productRepository.save(givenProduct);
// then
Assertions.assertThat(savedProduct.getNumber())
.isEqualTo(givenProduct.getNumber());
Assertions.assertThat(savedProduct.getName())
.isEqualTo(givenProduct.getName());
Assertions.assertThat(savedProduct.getPrice())
.isEqualTo(givenProduct.getPrice());
Assertions.assertThat(savedProduct.getStock())
.isEqualTo(givenProduct.getStock());
/* read */
// when
Product selectProduct = productRepository.findById(savedProduct.getNumber())
.orElseThrow(RuntimeException::new);
// then
Assertions.assertThat(selectProduct.getNumber())
.isEqualTo(givenProduct.getNumber());
Assertions.assertThat(selectProduct.getName())
.isEqualTo(givenProduct.getName());
Assertions.assertThat(selectProduct.getPrice())
.isEqualTo(givenProduct.getPrice());
Assertions.assertThat(selectProduct.getStock())
.isEqualTo(givenProduct.getStock());
/* update */
// when
Product foundProduct = productRepository.findById(selectProduct.getNumber())
.orElseThrow(RuntimeException::new);
foundProduct.setName("장난감");
Product updatedProduct = productRepository.save(foundProduct);
// then
assertEquals(updatedProduct.getName(), "장난감");
/* delete */
// when
productRepository.delete(updatedProduct);
// then
assertFalse(productRepository.findById(selectProduct.getNumber()).isPresent());
}
}
기본적인 메서드를 테스트 하기 때문에 Given 구문을 한 번만 사용해 전체 테스트에 활용했다
@SpringBootTest 어노테이션을 활용하면 테스트가 가능한가?
@SpringBootTest 어노테이션을 활용하면 스프링의 모든 설정을 가져오고 빈 객체도 전체를 스캔하기 때문에 의존성 주입에 대해 고민할 필요 없이 테스트가 가능
그러나 시간이 매우 오래 걸려서 효율적이지 않다
컨트롤러 객체의 테스트
컨트롤러란?
클라이언트에서 요청을 받음 → 서비스 컴포넌트로 요청을 전달 → 결과 값을 가공하여 응답
여러 레이어중 웹에 가장 가까히 있는 모듈
ProductController 를 대상으로 getProduct() 와 createProduct() 메서드에 대한 테스트 코드를 작성
ProductController - getProduct()
@RestController
@RequestMapping("/product")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService){
this.productService = productService;
}
@GetMapping()
public ResponseEntity<ProductResponseDto> getProduct(Long number){
ProductResponseDto productResponseDto = productService.getProduct(number);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
}
--- 생략 ---
}
ProductService의 객체를 의존성 주입받음
그런데 테스트 하는 입장에서 ProductController 만 테스트를 하고 싶다?
해결법 = Mock 객체 활용
test/java/com.springboot.test 패키지에 controllerr 생성, ProductControllerTest.java 파일 생성
package com.springboot.test.controller;
import com.springboot.test.data.dto.ProductResponseDto;
import com.springboot.test.service.impl.ProductServiceImpl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
ProductServiceImpl productService;
// <http://localhost:8080/api/v1/product-api/product/{productId}> 테스트 해볼것
@Test
@DisplayName("MockMvc를 통한 Product 데이터 가져오기 테스트")
void getProductTest() throws Exception {
given(productService.getProduct(123L)).willReturn(
new ProductResponseDto(123L, "pen", 5000, 2000));
String productId = "123";
mockMvc.perform(
get("/product?number=" + productId))
.andExpect(status().isOk())
.andExpect(jsonPath(
"$.number").exists())
.andExpect(jsonPath("$.name").exists())
.andExpect(jsonPath("$.price").exists())
.andExpect(jsonPath("$.stock").exists())
.andDo(print());
verify(productService).getProduct(123L);
}
@WebMvcTest(테스트 대상 클래스.class)
웹에서 사용되는 요청과 응답에 대한 테스트를 수행할 수 있다. 대상 클래스만 로드해 테스트를 수행,
만약 대상 클래스를 추가하지 않으면 @Controller, @RestController, @ContollerAdvice 등의 컨트롤러 관련 빈 객체가 모두 로드된다
@SpringBootTest보다 가볍게 테스트하기 위해 사용
@MockBean
MockBean은 실제 빈 객체가 아닌 Mock(가짜) 객체를 생성해서 주입하는 역할을 수행한다
MockBean 이 선언된 객체는 실제 객체가 아니기 때문에 실제 행위를 수행하지 않는다
그렇기 때문에 해당 객체는 개발자가 Mockito의 given() 메서드를 통해 동작을 정의해야 한다
@Test
테스트 코드가 포함돼 있다고 선언하는 어노테이션, JUnit Jupiter 에서는 이 어노테이션을 감지해서 테스트 계획이 포함시킨다
@DisplayName
테스트 메서드의 이름이 복작해서 가독성이 떨어질 경우 이 어노테이션을 통해 테스트에 대한 표현을 정의할 수 있다
일반적으로 @WebMvcTest 어노테이션을 사용한 테스트는 슬라이스 테스트(Slice test) 라고한다
레이어 별로 나누어서 테스트를 하는데, 컨트롤러는 Web과 맞닿아 있는 레이어 이기 때문에
외부요인을 차단하고 테스트하면 의미가 없다
ProductController에 작성되어 있는 createProduct() 메서드 구현
@RestController
@RequestMapping("/product")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService){
this.productService = productService;
}
@PostMapping()
public ResponseEntity<ProductResponseDto> createProduct(@RequestBody ProductDto productDto){
ProductResponseDto productResponseDto = productService.saveProduct(productDto);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
}
}
createProduct() 메서드는 @RequestBody로 값을 받고 있다.
이에 따른 테스트 코드
사용된 ProductDto의 코드
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ProductDto {
private String name;
private int price;
private int stock;
}
이렇게 하고서 실행하면 코드가 실행이 안된다
이유: Gson에 대한 의존성이 없기 때문
Gson = JSON 파생된 라이브러리 (구글 개발)
JSON 문자열로 변환하거나 JSON 문자열을 자바 객체로 변환
그러므로 pom.xml 파일에 의존성을 추가한다
<dependencies>
.. 내용 생략 ..
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
.. 내용 생략 ..
</dependencies>
서비스 객체의 테스트
DAO의 역할이 드러나지 않기 때문에 DAO는 생략
getProduct() 메서드의 테스트 코드 작성
test/java/com.springboot.test - service/impl 패키지를 생성하고 ProductServiceTest.java 파일을 생성한다
package com.springboot.test.service.impl;
import com.springboot.test.data.dto.ProductResponseDto;
import com.springboot.test.data.entity.Product;
import com.springboot.test.data.repository.ProductRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.Optional;
import static org.mockito.Mockito.verify;
public class ProductServiceTest {
private ProductRepository productRepository = Mockito.mock(ProductRepository.class);
private ProductServiceImpl productService;
@BeforeEach
public void setUpTest() {
productService = new ProductServiceImpl(productRepository);
}
@Test
void getProductTest() {
// given
Product givenProduct = new Product();
givenProduct.setNumber(123L);
givenProduct.setName("펜");
givenProduct.setPrice(1000);
givenProduct.setStock(1234);
Mockito.when(productRepository.findById(123L))
.thenReturn(Optional.of(givenProduct));
// when
ProductResponseDto productResponseDto = productService.getProduct(123L);
// then
Assertions.assertEquals(productResponseDto.getNumber(), givenProduct.getNumber());
Assertions.assertEquals(productResponseDto.getName(), givenProduct.getName());
Assertions.assertEquals(productResponseDto.getPrice(), givenProduct.getPrice());
Assertions.assertEquals(productResponseDto.getStock(), givenProduct.getStock());
verify(productRepository).findById(123L);
}
}
단위테스트를 위해서 외부요인을 모두 제거 → WebMvcTest 등의 어노테이션 x
이어서 saveProduce()의 단위 테스트
package com.springboot.test.service.impl;
import static org.mockito.AdditionalAnswers.returnsFirstArg; //추가
import static org.mockito.ArgumentMatchers.any; //추가
import static org.mockito.Mockito.verify;
import com.springboot.test.data.dto.ProductDto; //추가
import com.springboot.test.data.dto.ProductResponseDto;
import com.springboot.test.data.entity.Product;
import com.springboot.test.data.repository.ProductRepository;
import java.util.Optional;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
public class ProductServiceTest {
private ProductRepository productRepository = Mockito.mock(ProductRepository.class);
private ProductServiceImpl productService;
@BeforeEach
public void setUpTest() {
productService = new ProductServiceImpl(productRepository);
}
@Test
void saveProductTest() {
// given
Mockito.when(productRepository.save(any(Product.class)))
.then(returnsFirstArg());
// when
ProductResponseDto productResponseDto = productService.saveProduct(
new ProductDto("펜", 1000, 1234));
// then
Assertions.assertEquals(productResponseDto.getName(), "펜");
Assertions.assertEquals(productResponseDto.getPrice(), 1000);
Assertions.assertEquals(productResponseDto.getStock(), 1234);
verify(productRepository).save(any());
}
}
중요한것 - save(any(Product.class))
any()→ Mock객체의 동작 정의 or 검증하는 과정에서 특정한 매개변수를 전달하지 않고 매서드의 실행만 확인
MockBean 어노테이션을 사용한 테스트 환경 설정
... 위에있는 import ...
@ExtendWith(SpringExtension.class)
@Import({ProductServiceImpl.class})
class ProductServiceTest2 {
@MockBean
ProductRepository productRepository;
@Autowired
ProductService productService;
... test 코드 ...
기존- Mockito로 Mock객체를 SpringBean에 등록x, 객체를 직접 초기화 하여 사용
MockBean- 스프링에 Mock 객체를 등록하여 주입 받는 형식
리포지트리 객체의 테스트
개발자가 구현하는 레이어 중 DB와 가깝다
리포지토리 객체의 테스트 코드를 작성할 때 고려할점
1, findById(), save() 같은 기본 메서드에 대한 테스트는 큰 의미가 없다 -> 리포지토리의 기본 메서드는 테스트 검증을 마치고 제공된 것이기 때문
2, 데이터베이스의 연동 여부는 테스트 시 고려해 볼 사항이다 굳이 따지면 데이터베이스는 외부 요인에 속한다 만약 단위 테스트를 고려한다면 데이터베이스를 제외할 수 있고, 테스트 용으로 다른 데이터베이스를 사용하는 경우도 있다 -> 데이터베이스를 사용한 테스트는 테스트 과정에서 데이터베이스에 테스트 데이터가 적재되기 때문 ※적재된 데이터를 그냥 둘 수 없으므로 제거하는 코드까지 작성
3, 데이터베이스를 제외한 테스트 상황을 가정해서 테스트 데이터베이스로 H2 DB 를 사용, 테스트 할때는 마리아DB를 사용 JpaRepository에서 제공하는 기본 메서드 사용
H2 DB를 사용한 테스트 코드를 작성
pom.xml 파일에 의존성을 추가
<dependencies>
...생략...
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
...생략...
</dependencies>
데이터베이스에 값을 저장하는 테스트 코드 작성
test/com.springboot.test -> data/repository 패키지를 생성 -> ProductRepositoryTestByH2.java 파일을 생성
package com.springboot.test.data.repository;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.springboot.test.data.entity.Product;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@DataJpaTest
public class ProductRepositoryTestByH2 {
@Autowired
private ProductRepository productRepository;
@Test
void saveTest() {
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(1000);
Product savedProduct = productRepository.save(product);
assertEquals(product.getName(), savedProduct.getName());
assertEquals(product.getPrice(), savedProduct.getPrice());
assertEquals(product.getStock(), savedProduct.getStock());
}
}
※@DataJpaTest 어노테이션을 사용하고 있다
@DataJpaTest는 다음과 같은 기능을 제공
1, JPA와 관련된 설정만 로드해서 테스트를 진행 기본적으로 @Transactional 어노테이션을 포함하고 있어 테스트 코드가 종료되면 자동으로 데이터베이스의 롤백이 진행된다
2, 기본 값으로 임베디드 데이터베이스를 사용
다른 데이터베이스를 사용하라면 별도의 설정을 거쳐 사용 가능하다
이후 정상적인 테스트가 이뤄졌는지 체크
save() 메서드의 리턴 객체와 Given에서 생성한 엔티티 객체의 값이 일치하는지 assertEquals() 메서드를 통해 검증
package com.springboot.test.data.repository;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.springboot.test.data.entity.Product;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
//@TestPropertySource("classpath:application-test.properties")
@DataJpaTest
public class ProductRepositoryTestByH2 {
@Autowired
private ProductRepository productRepository;
@Test
void selectTest() {
// given
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(1000);
Product savedProduct = productRepository.saveAndFlush(product);
// when
Product foundProduct = productRepository.findById(savedProduct.getNumber()).get();
// then
assertEquals(product.getName(), foundProduct.getName());
assertEquals(product.getPrice(), foundProduct.getPrice());
assertEquals(product.getStock(), foundProduct.getStock());
}
}
마리아DB에서 테스트하기 위해서는 별도의 설정이 필요
같은 패키지 경로
test/com.springboot.test -> data/repository 패키지 - ProductRepositoryTest.java 파일을 생성하고 아래와 같이 코드를 작성
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Test
void save() {
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(1000);
Product savedProduct = productRepository.save(product);
assertEquals(product.getName(), savedProduct.getName());
assertEquals(product.getPrice(), savedProduct.getPrice());
assertEquals(product.getStock(), savedProduct.getStock());
}
}
replace 요소는 @AutoConfigureTestDatabase 어노테이션의 값을 조정하는 작업을 수행한다
replace 속성의 기본값은 Replcae.ANY 이며, 이 경우 임베디드 메모리 데이터베이스를 사용한다
이 속성값을 Replace.NONE 으로 변경하면 애플리케이션에서 실제로 사용하는 데이터베이스로 테스트 가능
여기까지 하면 @DataJpaTest를 사용하지 않고 @SpringBootTest 어노테이션 으로도 테스트 가능
같은 패키지 경로에 ProductRepositoryTest2.java 파일을 생성
@SpringBootTest
public class ProductRepositoryTest2
{
@Autowired
ProductRepository productRepository;
@Test
public void basicCRUDTest() {
/* create */
// given
Product givenProduct = Product.builder()
.name("노트")
.price(1000)
.stock(500)
.build();
// when
Product savedProduct = productRepository.save(givenProduct);
// then
Assertions.assertThat(savedProduct.getNumber())
.isEqualTo(givenProduct.getNumber());
Assertions.assertThat(savedProduct.getName())
.isEqualTo(givenProduct.getName());
Assertions.assertThat(savedProduct.getPrice())
.isEqualTo(givenProduct.getPrice());
Assertions.assertThat(savedProduct.getStock())
.isEqualTo(givenProduct.getStock());
/* read */
// when
Product selectProduct = productRepository.findById(savedProduct.getNumber())
.orElseThrow(RuntimeException::new);
// then
Assertions.assertThat(selectProduct.getNumber())
.isEqualTo(givenProduct.getNumber());
Assertions.assertThat(selectProduct.getName())
.isEqualTo(givenProduct.getName());
Assertions.assertThat(selectProduct.getPrice())
.isEqualTo(givenProduct.getPrice());
Assertions.assertThat(selectProduct.getStock())
.isEqualTo(givenProduct.getStock());
/* update */
// when
Product foundProduct = productRepository.findById(selectProduct.getNumber())
.orElseThrow(RuntimeException::new);
foundProduct.setName("장난감");
Product updatedProduct = productRepository.save(foundProduct);
// then
assertEquals(updatedProduct.getName(), "장난감");
/* delete */
// when
productRepository.delete(updatedProduct);
// then
assertFalse(productRepository.findById(selectProduct.getNumber()).isPresent());
}
}
기본적인 메서드를 테스트 하기 때문에 Given 구문을 한 번만 사용해 전체 테스트에 활용했다
@SpringBootTest 어노테이션을 활용하면 테스트가 가능한가?
@SpringBootTest 어노테이션을 활용하면 스프링의 모든 설정을 가져오고 빈 객체도 전체를 스캔하기 때문에 의존성 주입에 대해 고민할 필요 없이 테스트가 가능
그러나 시간이 매우 오래 걸려서 효율적이지 않다
JaCoCo를 활용한 테스트 커버리지 확인
코드 커버리지(code coverage)
소프트웨어의 테스트 수준이 충분한지를 표현하는 지표 중 하나이다
테스트를 진행했을 때 대상 코드가 실행됐는지 표현하는 방법으로도 사용된다
JaCoCo 플러그인 설정
pom.xml 파일에서 의존성을 추가
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
</dependency>
다음의 플러그인 설정을 pom.xml 파일 내 <build> 태그 안에 추가
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<configuration>
<excludes>
<exclude>**/ProductServiceImpl.class</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>jacoco-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<elment>BUNDLE</elment>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
<element>METHOD</element>
<limits>
<limit>
<counter>LINE</counter>
<value>TOTALCOUNT</value>
<maximum>50</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
위의 코드 중 <configuration> 태그 살펴보기
<configuration>
<excludes>
<exclude>**/ProductServiceImpl.class</exclude>
</excludes>
</configuration>
일부의 클래스를 커버리지 측정 대상에서 제외하는 것
해당 코드에서는 ProductServiceImpl.class 를 제외
<executions> 태그에 설정하는 플러그인의 실행 내용
<excurion>은 기본적으로 <goal>을 포함, 설정한 값에 따라 추가 설정이 필요한 내용을 <configuration>과 <rule>을 통해 작성
<execution>에서 설정할 수 있는 <goal>의 속성값
help : jacoco-maven-plugin에 대한 도움말을 보여준다
prepare-agent : 테스트 중인 애플리케이션에 VM 인수를 전달하는 JaCoCo 런타임 에이전트 속성을 준비한다 에이전트는 maven-surefire-plugin을 통해 테스트한 결과를 가져오는 역할을 수행한다
prepare-agent-integration : prepare-agent와 유사하지만 통합 테스트에 적합한 기본값을 제공한다
merge : 실행 데이터 파일 세트(.exec)를 단일 파일로 병합한다
report : 단일 프로젝트 테스트를 마치면 생성되는 코드 검사 보고서를 다양한 형식(HTML, XML, CSV) 중에서 선택할 수 있게 한다
report-integration : report와 유사하나 통합 테스트에 적합한 기본값을 제공한다 report-aggregate : Reactor 내의 여러 프로젝트에서 구조화된 보고서(HTML, XML, CSV)를 생성한다 보고서는 해당 프로젝트가 의존하는 모듈에서 생성
check : 코드 커버리지의 메트릭 충족 여부를 검사한다 메트릭은 테스트 커버리지를 측정하는 데 필요한 지표를 의미하고, 메트릭은 check가 설정된 <exceution> 태그 내에서 <rule>을 통해 설정
dump : TCP 서버 모드에서 실행 중인 JaCoCo 에이전트에서 TCP/IP를 통한 덤프를 생성
instrument : 오프라인 특정을 수행하는 명령, 테스트를 실행한 후 restore-instrumented-classes Coal로 원본 클래스 파일들을 저장해야 한다
restore-instrumented-class : 오프라인 측정 전 원본 파일을 저장하는 기능을 수행한다
JaCoCo에서 설정할 수 있는 Rule
<cofiguration> 태그 안에 설정하며 다양한 속성을 활용할 수 있다
Element는 코드 커버리지를 체크하는 데 필요한 범위 기준을 설정하고, 6가지의 속성이 있다
BUNDLE(기본값) : 패키지 번들(프로젝트 내 모든 파일) PACKAGE : 패키지 CLASS : 클래스 GROUP : 논리적 번들 그룹 SOURCEFILE : 소스 파일 METHOD : 메서드
Counter는 커버리지를 측정하는 데 사용하는 지표이다 Counter에서 사용할 수 있는 커버리지의 측정 단위는 총 6가지다
LINE : 빈 줄을 제외한 실제 코드의 라인 수 BRANCH : 조건문 등의 분기 수 CLASS : 클래스 수 METHOD : 메서드 수 INSTRUCTION(기본값) : 자바의 바이트코드 명령 수 COMPLEXITY : 복잡도, 복잡도는 맥케이브 순환 복잡도 정의를 따른다. TOTALCOUNT : 전체 개수 MISSDCOUNT : 커버되지 않은 개수 COVEREDCOUNT : 커버된 개수 MISSEDRATIO : 커버되지 않은 비율 COVEREDRATIO(기본값) : 커버된 비율
예제 보기
<configuration>
<rules>
<rule>
<elment>BUNDLE</elment>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
<element>METHOD</element>
<limits>
<limit>
<counter>LINE</counter>
<value>TOTALCOUNT</value>
<maximum>50</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
4~11 줄을 보게 되면 번들 단위의 바이트코드 명령을 최소 80% 달성 하게끔 리미트를 설정했다.
12~19 줄을 보면 메서드 단위로 전체 라인수를 최대 50줄 까지만 되게끔 설정을 했다, 50줄을 넘게되면 에러가 발생 할 것이다
JaCoCo 테스트 커버리지 확인
JaCoCo 플러그인으로 테스트 커버리지를 측정하려면 메이븐의 테스트 단계가 선행돼야 한다 메이븐의 생명주기는 아래와 같이 Maven 탭에서 확인할 수 있으며, JaCoCo는 test 단계 뒤에 있는 package 단계에서 실행할 수 있다
package를 더블클릭 하여 빌드를 진행 할 시 target 폴더 내 site → jacoco폴더가 생성된다
※정상적으로 되지 않는 경우: 프로젝트 경로에 한글이 없는지 확인이 필요하다(개인적으로 이거 몰라서 고생 많이함..)
기본적으로 JaCoCo 리포트 파일은 HTML, CSV, XMl 형식으로 제공된다 일반적으로 곧바로 보고서를 보기 위해서는 HTML 파일을 주로 이용한다 웹 브라우저를 통해 HTML 파일을 열면 아래와 같이 리포트 결과를 확인할 수 있다
Element : 우측 테스트 커버리지를 측정한 단위를 표현한다
링크를 따라 들어가면 세부 사항을 볼 수 있다
Missed Instructions - Cov.(Coverage) : 테스트를 수행한 후 바이트코드의 커버리지를 퍼센티지와 바(Bar) 형식으로 제공한다
Missed Branches - Cov.(Coverage) : 분기에 대한 테스트 커버리지를 퍼센티지와 바 형식으로 제공한다
Missed - Cxty(Complexity) : 복잡도에 대한 커버리지 대상 개수와 커버되지 않은 수를 제공한다
Missed - Lines : 테스트 대상 라인 수와 커버되지 않은 라인 수를 제공한다
Missed - Methods : 테스트 대상 메서드 수와 커버되지 않은 메서드 수를 제공한다
Messed - Classes : 테스트 대상 클래스 수와 커버되지 않은 메서드 수를 제공한다
세부 내용 살펴보기
ProductController 살펴보기
HTML 파일에서 com.springboot.test.controller 패키지를 누르면 ProductController클래스가 Element에 표시되며 테스트 커버리지 통계를 보여준다 다시 해당 부분을 클릭하면 컨트롤러에 작성돼 있는 Method가 Element의 기준이 되고, 여기서 메서드를 클릭하면 아래와 같이 코드 레벨 에서의 테스트 커버리지를 확인할 수 있다
과제: 직접 실행해서 밑의 상태처럼 되는지 확인하기
JaCoCo를 활용한 테스트 커버리지 확인
코드 커버리지(code coverage)
소프트웨어의 테스트 수준이 충분한지를 표현하는 지표 중 하나이다
테스트를 진행했을 때 대상 코드가 실행됐는지 표현하는 방법으로도 사용된다
JaCoCo 플러그인 설정
pom.xml 파일에서 의존성을 추가
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
</dependency>
다음의 플러그인 설정을 pom.xml 파일 내 <build> 태그 안에 추가
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<configuration>
<excludes>
<exclude>**/ProductServiceImpl.class</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>jacoco-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<elment>BUNDLE</elment>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
<element>METHOD</element>
<limits>
<limit>
<counter>LINE</counter>
<value>TOTALCOUNT</value>
<maximum>50</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
위의 코드 중 <configuration> 태그 살펴보기
<configuration>
<excludes>
<exclude>**/ProductServiceImpl.class</exclude>
</excludes>
</configuration>
일부의 클래스를 커버리지 측정 대상에서 제외하는 것
해당 코드에서는 ProductServiceImpl.class 를 제외
<executions> 태그에 설정하는 플러그인의 실행 내용
<excurion>은 기본적으로 <goal>을 포함, 설정한 값에 따라 추가 설정이 필요한 내용을 <configuration>과 <rule>을 통해 작성
<execution>에서 설정할 수 있는 <goal>의 속성값
help : jacoco-maven-plugin에 대한 도움말을 보여준다
prepare-agent : 테스트 중인 애플리케이션에 VM 인수를 전달하는 JaCoCo 런타임 에이전트 속성을 준비한다 에이전트는 maven-surefire-plugin을 통해 테스트한 결과를 가져오는 역할을 수행한다
prepare-agent-integration : prepare-agent와 유사하지만 통합 테스트에 적합한 기본값을 제공한다
merge : 실행 데이터 파일 세트(.exec)를 단일 파일로 병합한다
report : 단일 프로젝트 테스트를 마치면 생성되는 코드 검사 보고서를 다양한 형식(HTML, XML, CSV) 중에서 선택할 수 있게 한다
report-integration : report와 유사하나 통합 테스트에 적합한 기본값을 제공한다 report-aggregate : Reactor 내의 여러 프로젝트에서 구조화된 보고서(HTML, XML, CSV)를 생성한다 보고서는 해당 프로젝트가 의존하는 모듈에서 생성
check : 코드 커버리지의 메트릭 충족 여부를 검사한다 메트릭은 테스트 커버리지를 측정하는 데 필요한 지표를 의미하고, 메트릭은 check가 설정된 <exceution> 태그 내에서 <rule>을 통해 설정
dump : TCP 서버 모드에서 실행 중인 JaCoCo 에이전트에서 TCP/IP를 통한 덤프를 생성
instrument : 오프라인 특정을 수행하는 명령, 테스트를 실행한 후 restore-instrumented-classes Coal로 원본 클래스 파일들을 저장해야 한다
restore-instrumented-class : 오프라인 측정 전 원본 파일을 저장하는 기능을 수행한다
JaCoCo에서 설정할 수 있는 Rule
<cofiguration> 태그 안에 설정하며 다양한 속성을 활용할 수 있다
Element는 코드 커버리지를 체크하는 데 필요한 범위 기준을 설정하고, 6가지의 속성이 있다
BUNDLE(기본값) : 패키지 번들(프로젝트 내 모든 파일) PACKAGE : 패키지 CLASS : 클래스 GROUP : 논리적 번들 그룹 SOURCEFILE : 소스 파일 METHOD : 메서드
Counter는 커버리지를 측정하는 데 사용하는 지표이다 Counter에서 사용할 수 있는 커버리지의 측정 단위는 총 6가지다
LINE : 빈 줄을 제외한 실제 코드의 라인 수 BRANCH : 조건문 등의 분기 수 CLASS : 클래스 수 METHOD : 메서드 수 INSTRUCTION(기본값) : 자바의 바이트코드 명령 수 COMPLEXITY : 복잡도, 복잡도는 맥케이브 순환 복잡도 정의를 따른다. TOTALCOUNT : 전체 개수 MISSDCOUNT : 커버되지 않은 개수 COVEREDCOUNT : 커버된 개수 MISSEDRATIO : 커버되지 않은 비율 COVEREDRATIO(기본값) : 커버된 비율
예제 보기
<configuration>
<rules>
<rule>
<elment>BUNDLE</elment>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
<element>METHOD</element>
<limits>
<limit>
<counter>LINE</counter>
<value>TOTALCOUNT</value>
<maximum>50</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
4~11 줄을 보게 되면 번들 단위의 바이트코드 명령을 최소 80% 달성 하게끔 리미트를 설정했다.
12~19 줄을 보면 메서드 단위로 전체 라인수를 최대 50줄 까지만 되게끔 설정을 했다, 50줄을 넘게되면 에러가 발생 할 것이다
JaCoCo 테스트 커버리지 확인
JaCoCo 플러그인으로 테스트 커버리지를 측정하려면 메이븐의 테스트 단계가 선행돼야 한다 메이븐의 생명주기는 아래와 같이 Maven 탭에서 확인할 수 있으며, JaCoCo는 test 단계 뒤에 있는 package 단계에서 실행할 수 있다
package를 더블클릭 하여 빌드를 진행 할 시 target 폴더 내 site → jacoco폴더가 생성된다
※정상적으로 되지 않는 경우: 프로젝트 경로에 한글이 없는지 확인이 필요하다(개인적으로 이거 몰라서 고생 많이함..)
기본적으로 JaCoCo 리포트 파일은 HTML, CSV, XMl 형식으로 제공된다 일반적으로 곧바로 보고서를 보기 위해서는 HTML 파일을 주로 이용한다 웹 브라우저를 통해 HTML 파일을 열면 아래와 같이 리포트 결과를 확인할 수 있다
Element : 우측 테스트 커버리지를 측정한 단위를 표현한다
링크를 따라 들어가면 세부 사항을 볼 수 있다
Missed Instructions - Cov.(Coverage) : 테스트를 수행한 후 바이트코드의 커버리지를 퍼센티지와 바(Bar) 형식으로 제공한다
Missed Branches - Cov.(Coverage) : 분기에 대한 테스트 커버리지를 퍼센티지와 바 형식으로 제공한다
Missed - Cxty(Complexity) : 복잡도에 대한 커버리지 대상 개수와 커버되지 않은 수를 제공한다
Missed - Lines : 테스트 대상 라인 수와 커버되지 않은 라인 수를 제공한다
Missed - Methods : 테스트 대상 메서드 수와 커버되지 않은 메서드 수를 제공한다
Messed - Classes : 테스트 대상 클래스 수와 커버되지 않은 메서드 수를 제공한다
세부 내용 살펴보기
ProductController 살펴보기
HTML 파일에서 com.springboot.test.controller 패키지를 누르면 ProductController클래스가 Element에 표시되며 테스트 커버리지 통계를 보여준다 다시 해당 부분을 클릭하면 컨트롤러에 작성돼 있는 Method가 Element의 기준이 되고, 여기서 메서드를 클릭하면 아래와 같이 코드 레벨 에서의 테스트 커버리지를 확인할 수 있다
과제: 직접 실행해서 밑의 상태처럼 되는지 확인하기
JaCoCo를 활용한 테스트 커버리지 확인
코드 커버리지(code coverage)
소프트웨어의 테스트 수준이 충분한지를 표현하는 지표 중 하나이다
테스트를 진행했을 때 대상 코드가 실행됐는지 표현하는 방법으로도 사용된다
JaCoCo 플러그인 설정
pom.xml 파일에서 의존성을 추가
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
</dependency>
다음의 플러그인 설정을 pom.xml 파일 내 <build> 태그 안에 추가
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<configuration>
<excludes>
<exclude>**/ProductServiceImpl.class</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>jacoco-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<elment>BUNDLE</elment>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
<element>METHOD</element>
<limits>
<limit>
<counter>LINE</counter>
<value>TOTALCOUNT</value>
<maximum>50</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
위의 코드 중 <configuration> 태그 살펴보기
<configuration>
<excludes>
<exclude>**/ProductServiceImpl.class</exclude>
</excludes>
</configuration>
일부의 클래스를 커버리지 측정 대상에서 제외하는 것
해당 코드에서는 ProductServiceImpl.class 를 제외
<executions> 태그에 설정하는 플러그인의 실행 내용
<excurion>은 기본적으로 <goal>을 포함, 설정한 값에 따라 추가 설정이 필요한 내용을 <configuration>과 <rule>을 통해 작성
<execution>에서 설정할 수 있는 <goal>의 속성값
help : jacoco-maven-plugin에 대한 도움말을 보여준다
prepare-agent : 테스트 중인 애플리케이션에 VM 인수를 전달하는 JaCoCo 런타임 에이전트 속성을 준비한다 에이전트는 maven-surefire-plugin을 통해 테스트한 결과를 가져오는 역할을 수행한다
prepare-agent-integration : prepare-agent와 유사하지만 통합 테스트에 적합한 기본값을 제공한다
merge : 실행 데이터 파일 세트(.exec)를 단일 파일로 병합한다
report : 단일 프로젝트 테스트를 마치면 생성되는 코드 검사 보고서를 다양한 형식(HTML, XML, CSV) 중에서 선택할 수 있게 한다
report-integration : report와 유사하나 통합 테스트에 적합한 기본값을 제공한다 report-aggregate : Reactor 내의 여러 프로젝트에서 구조화된 보고서(HTML, XML, CSV)를 생성한다 보고서는 해당 프로젝트가 의존하는 모듈에서 생성
check : 코드 커버리지의 메트릭 충족 여부를 검사한다 메트릭은 테스트 커버리지를 측정하는 데 필요한 지표를 의미하고, 메트릭은 check가 설정된 <exceution> 태그 내에서 <rule>을 통해 설정
dump : TCP 서버 모드에서 실행 중인 JaCoCo 에이전트에서 TCP/IP를 통한 덤프를 생성
instrument : 오프라인 특정을 수행하는 명령, 테스트를 실행한 후 restore-instrumented-classes Coal로 원본 클래스 파일들을 저장해야 한다
restore-instrumented-class : 오프라인 측정 전 원본 파일을 저장하는 기능을 수행한다
JaCoCo에서 설정할 수 있는 Rule
<cofiguration> 태그 안에 설정하며 다양한 속성을 활용할 수 있다
Element는 코드 커버리지를 체크하는 데 필요한 범위 기준을 설정하고, 6가지의 속성이 있다
BUNDLE(기본값) : 패키지 번들(프로젝트 내 모든 파일) PACKAGE : 패키지 CLASS : 클래스 GROUP : 논리적 번들 그룹 SOURCEFILE : 소스 파일 METHOD : 메서드
Counter는 커버리지를 측정하는 데 사용하는 지표이다 Counter에서 사용할 수 있는 커버리지의 측정 단위는 총 6가지다
LINE : 빈 줄을 제외한 실제 코드의 라인 수 BRANCH : 조건문 등의 분기 수 CLASS : 클래스 수 METHOD : 메서드 수 INSTRUCTION(기본값) : 자바의 바이트코드 명령 수 COMPLEXITY : 복잡도, 복잡도는 맥케이브 순환 복잡도 정의를 따른다. TOTALCOUNT : 전체 개수 MISSDCOUNT : 커버되지 않은 개수 COVEREDCOUNT : 커버된 개수 MISSEDRATIO : 커버되지 않은 비율 COVEREDRATIO(기본값) : 커버된 비율
예제 보기
<configuration>
<rules>
<rule>
<elment>BUNDLE</elment>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
<element>METHOD</element>
<limits>
<limit>
<counter>LINE</counter>
<value>TOTALCOUNT</value>
<maximum>50</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
4~11 줄을 보게 되면 번들 단위의 바이트코드 명령을 최소 80% 달성 하게끔 리미트를 설정했다.
12~19 줄을 보면 메서드 단위로 전체 라인수를 최대 50줄 까지만 되게끔 설정을 했다, 50줄을 넘게되면 에러가 발생 할 것이다
JaCoCo 테스트 커버리지 확인
JaCoCo 플러그인으로 테스트 커버리지를 측정하려면 메이븐의 테스트 단계가 선행돼야 한다 메이븐의 생명주기는 아래와 같이 Maven 탭에서 확인할 수 있으며, JaCoCo는 test 단계 뒤에 있는 package 단계에서 실행할 수 있다
package를 더블클릭 하여 빌드를 진행 할 시 target 폴더 내 site → jacoco폴더가 생성된다
※정상적으로 되지 않는 경우: 프로젝트 경로에 한글이 없는지 확인이 필요하다(개인적으로 이거 몰라서 고생 많이함..)
기본적으로 JaCoCo 리포트 파일은 HTML, CSV, XMl 형식으로 제공된다 일반적으로 곧바로 보고서를 보기 위해서는 HTML 파일을 주로 이용한다 웹 브라우저를 통해 HTML 파일을 열면 아래와 같이 리포트 결과를 확인할 수 있다
Element : 우측 테스트 커버리지를 측정한 단위를 표현한다
링크를 따라 들어가면 세부 사항을 볼 수 있다
Missed Instructions - Cov.(Coverage) : 테스트를 수행한 후 바이트코드의 커버리지를 퍼센티지와 바(Bar) 형식으로 제공한다
Missed Branches - Cov.(Coverage) : 분기에 대한 테스트 커버리지를 퍼센티지와 바 형식으로 제공한다
Missed - Cxty(Complexity) : 복잡도에 대한 커버리지 대상 개수와 커버되지 않은 수를 제공한다
Missed - Lines : 테스트 대상 라인 수와 커버되지 않은 라인 수를 제공한다
Missed - Methods : 테스트 대상 메서드 수와 커버되지 않은 메서드 수를 제공한다
Messed - Classes : 테스트 대상 클래스 수와 커버되지 않은 메서드 수를 제공한다
세부 내용 살펴보기
ProductController 살펴보기
HTML 파일에서 com.springboot.test.controller 패키지를 누르면 ProductController클래스가 Element에 표시되며 테스트 커버리지 통계를 보여준다 다시 해당 부분을 클릭하면 컨트롤러에 작성돼 있는 Method가 Element의 기준이 되고, 여기서 메서드를 클릭하면 아래와 같이 코드 레벨 에서의 테스트 커버리지를 확인할 수 있다
과제: 직접 실행해서 밑의 상태처럼 되는지 확인하기
JaCoCo를 활용한 테스트 커버리지 확인
코드 커버리지(code coverage)
소프트웨어의 테스트 수준이 충분한지를 표현하는 지표 중 하나이다
테스트를 진행했을 때 대상 코드가 실행됐는지 표현하는 방법으로도 사용된다
JaCoCo 플러그인 설정
pom.xml 파일에서 의존성을 추가
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
</dependency>
다음의 플러그인 설정을 pom.xml 파일 내 <build> 태그 안에 추가
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<configuration>
<excludes>
<exclude>**/ProductServiceImpl.class</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>jacoco-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<elment>BUNDLE</elment>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
<element>METHOD</element>
<limits>
<limit>
<counter>LINE</counter>
<value>TOTALCOUNT</value>
<maximum>50</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
위의 코드 중 <configuration> 태그 살펴보기
<configuration>
<excludes>
<exclude>**/ProductServiceImpl.class</exclude>
</excludes>
</configuration>
일부의 클래스를 커버리지 측정 대상에서 제외하는 것
해당 코드에서는 ProductServiceImpl.class 를 제외
<executions> 태그에 설정하는 플러그인의 실행 내용
<excurion>은 기본적으로 <goal>을 포함, 설정한 값에 따라 추가 설정이 필요한 내용을 <configuration>과 <rule>을 통해 작성
<execution>에서 설정할 수 있는 <goal>의 속성값
help : jacoco-maven-plugin에 대한 도움말을 보여준다
prepare-agent : 테스트 중인 애플리케이션에 VM 인수를 전달하는 JaCoCo 런타임 에이전트 속성을 준비한다 에이전트는 maven-surefire-plugin을 통해 테스트한 결과를 가져오는 역할을 수행한다
prepare-agent-integration : prepare-agent와 유사하지만 통합 테스트에 적합한 기본값을 제공한다
merge : 실행 데이터 파일 세트(.exec)를 단일 파일로 병합한다
report : 단일 프로젝트 테스트를 마치면 생성되는 코드 검사 보고서를 다양한 형식(HTML, XML, CSV) 중에서 선택할 수 있게 한다
report-integration : report와 유사하나 통합 테스트에 적합한 기본값을 제공한다 report-aggregate : Reactor 내의 여러 프로젝트에서 구조화된 보고서(HTML, XML, CSV)를 생성한다 보고서는 해당 프로젝트가 의존하는 모듈에서 생성
check : 코드 커버리지의 메트릭 충족 여부를 검사한다 메트릭은 테스트 커버리지를 측정하는 데 필요한 지표를 의미하고, 메트릭은 check가 설정된 <exceution> 태그 내에서 <rule>을 통해 설정
dump : TCP 서버 모드에서 실행 중인 JaCoCo 에이전트에서 TCP/IP를 통한 덤프를 생성
instrument : 오프라인 특정을 수행하는 명령, 테스트를 실행한 후 restore-instrumented-classes Coal로 원본 클래스 파일들을 저장해야 한다
restore-instrumented-class : 오프라인 측정 전 원본 파일을 저장하는 기능을 수행한다
JaCoCo에서 설정할 수 있는 Rule
<cofiguration> 태그 안에 설정하며 다양한 속성을 활용할 수 있다
Element는 코드 커버리지를 체크하는 데 필요한 범위 기준을 설정하고, 6가지의 속성이 있다
BUNDLE(기본값) : 패키지 번들(프로젝트 내 모든 파일) PACKAGE : 패키지 CLASS : 클래스 GROUP : 논리적 번들 그룹 SOURCEFILE : 소스 파일 METHOD : 메서드
Counter는 커버리지를 측정하는 데 사용하는 지표이다 Counter에서 사용할 수 있는 커버리지의 측정 단위는 총 6가지다
LINE : 빈 줄을 제외한 실제 코드의 라인 수 BRANCH : 조건문 등의 분기 수 CLASS : 클래스 수 METHOD : 메서드 수 INSTRUCTION(기본값) : 자바의 바이트코드 명령 수 COMPLEXITY : 복잡도, 복잡도는 맥케이브 순환 복잡도 정의를 따른다. TOTALCOUNT : 전체 개수 MISSDCOUNT : 커버되지 않은 개수 COVEREDCOUNT : 커버된 개수 MISSEDRATIO : 커버되지 않은 비율 COVEREDRATIO(기본값) : 커버된 비율
예제 보기
<configuration>
<rules>
<rule>
<elment>BUNDLE</elment>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
<element>METHOD</element>
<limits>
<limit>
<counter>LINE</counter>
<value>TOTALCOUNT</value>
<maximum>50</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
4~11 줄을 보게 되면 번들 단위의 바이트코드 명령을 최소 80% 달성 하게끔 리미트를 설정했다.
12~19 줄을 보면 메서드 단위로 전체 라인수를 최대 50줄 까지만 되게끔 설정을 했다, 50줄을 넘게되면 에러가 발생 할 것이다
JaCoCo 테스트 커버리지 확인
JaCoCo 플러그인으로 테스트 커버리지를 측정하려면 메이븐의 테스트 단계가 선행돼야 한다 메이븐의 생명주기는 아래와 같이 Maven 탭에서 확인할 수 있으며, JaCoCo는 test 단계 뒤에 있는 package 단계에서 실행할 수 있다
package를 더블클릭 하여 빌드를 진행 할 시 target 폴더 내 site → jacoco폴더가 생성된다
※정상적으로 되지 않는 경우: 프로젝트 경로에 한글이 없는지 확인이 필요하다(개인적으로 이거 몰라서 고생 많이함..)
기본적으로 JaCoCo 리포트 파일은 HTML, CSV, XMl 형식으로 제공된다 일반적으로 곧바로 보고서를 보기 위해서는 HTML 파일을 주로 이용한다 웹 브라우저를 통해 HTML 파일을 열면 아래와 같이 리포트 결과를 확인할 수 있다
Element : 우측 테스트 커버리지를 측정한 단위를 표현한다
링크를 따라 들어가면 세부 사항을 볼 수 있다
Missed Instructions - Cov.(Coverage) : 테스트를 수행한 후 바이트코드의 커버리지를 퍼센티지와 바(Bar) 형식으로 제공한다
Missed Branches - Cov.(Coverage) : 분기에 대한 테스트 커버리지를 퍼센티지와 바 형식으로 제공한다
Missed - Cxty(Complexity) : 복잡도에 대한 커버리지 대상 개수와 커버되지 않은 수를 제공한다
Missed - Lines : 테스트 대상 라인 수와 커버되지 않은 라인 수를 제공한다
Missed - Methods : 테스트 대상 메서드 수와 커버되지 않은 메서드 수를 제공한다
Messed - Classes : 테스트 대상 클래스 수와 커버되지 않은 메서드 수를 제공한다
세부 내용 살펴보기
ProductController 살펴보기
HTML 파일에서 com.springboot.test.controller 패키지를 누르면 ProductController클래스가 Element에 표시되며 테스트 커버리지 통계를 보여준다 다시 해당 부분을 클릭하면 컨트롤러에 작성돼 있는 Method가 Element의 기준이 되고, 여기서 메서드를 클릭하면 아래와 같이 코드 레벨 에서의 테스트 커버리지를 확인할 수 있다
과제: 직접 실행해서 밑의 상태처럼 되는지 확인하기