우리가 작성한 코드나 비즈니스 로직 자체를 테스트하기 위해 작성한 코드
📌 테스트 대상 범위를 기준으로 구분
애플리케이션의 개별 모듈을 독립적으로 테스트하는 방식
테스트 대상의 범위를 기준으로 가장 작은 단위의 테스트 방식
일반적으로 메서드 단위로 테스트 수행
테스트 비용이 적어 피드백 빠름
애플리케이션을 구성하는 다양한 모듈을 결합해 전체적인 로직이 의도한 대로 동작하는지 테스트하는 방식
모듈을 통합하는 과정에서의 호환성 등을 포함해 애플리케이션이 정상적으로 동작하는지 확인하기 위해 수행하는 테스트 방식
데이터베이스나 네트워크 같은 외부 요인들을 포함하고 테스트 진행
테스트 비용이 큼
테스트 비용: 금전적인 비용 포함, 시간, 인력과 같은 개발에 필요한 것들 포괄
하나의 서비스를 개발할 때 개발 과정에서 60%, 테스트 과정에서 40% 비용이 듦
테스트 주도 개발에서 파생된 BDD(Behavior-Driven-Development: 행위 주도 개발)를 통해 탄생한 테스트 접근 방식.
테스트 수행 전 테스트에 필요한 환경 설정을 하는 단계
테스트에 필요한 변수 정의, Mock 객체 통해 특정 상황에 대한 행동 정의
테스트의 목적을 보여주는 단계
실제 테스트 코드 포함
테스트를 통한 결과값을 가져옴
테스트 결과를 검증하는 단계
When 단계에서 나온 결과 검증
자바 언어에서 사용되는 대표적인 테스트 프레임워크, 테스트를 위한 도구 제공
어노테이션 기반의 테스트 방식을 지원하는 것이 가장 큰 특징
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 // 테스트 실행X
void test3() {
System.out.println("## test3 시작 ##");
System.out.println();
}
}
## BeforeAll Annotation 호출 ##
## BeforeEach Annotation 호출 ##
## test1 시작 ##
## AfterEach Annotation 호출 ##
## BeforeEach Annotation 호출 ##
## test2 시작 ##
## AfterEach Annotation 호출 ##
void com.springboot.jpa.TestLifeCycle.test3() is @Disabled
## AfterAll Annotation 호출 ##
import com.springboot.jpa.data.dto.ProductResponseDto;
import com.springboot.jpa.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 org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
@WebMvcTest(ProductController.class)
public class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
ProductServiceImpl productService; // Mock 객체 주입
@Test
@DisplayName("MockMvc를 통한 Product 데이터 가져오기 테스트")
void getProductTest() throws Exception {
given(productService.getProduct(123L)).willReturn(
new ProductResponseDto(123L, "pen", 5000, 2000)
);
String productId = "123";
// mockMvc - api 테스트를 위한 객체
// 서블릿 컨테이너의 구동 없이 가상의 MVC 환경에서 모의 HTTP 서블릿을 요청하는 유틸리티 클래스
mockMvc.perform(
MockMvcRequestBuilders.get("/product?number="+productId)
)
// 결과값 검증
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.number").exists())
.andExpect(MockMvcResultMatchers.jsonPath("$.name").exists())
.andExpect(MockMvcResultMatchers.jsonPath("$.price").exists())
.andExpect(MockMvcResultMatchers.jsonPath("$.stock").exists())
.andDo(MockMvcResultHandlers.print()); // 요청과 응답의 전체 내용 확인
// 지정된 메서드가 실행됐는지 검증
verify(productService).getProduct(123L);
}
}
@WebMvcTest(테스트 대상 클래스.class)
웹에서 사용되는 요청과 응답에 대한 테스트 수행. 대상 클래스만 로드해 테스트 수행.
대상 클래스 추가하지 않으면 컨트롤러 관련 빈 객체 모두 로드
@SpringBootTest보다 가볍게 테스트하기 위해 사용 (슬라이스 테스트 - 레이어드 아키텍처를 기준으로 각 레이어별로 나누어 테스트 진행)
@MockBean
실제 빈 객체가 아닌 Mock(가짜) 객체를 생성해 주입하는 역할 수행
실제 행위X -> 개발자가 Mockito의 given() 메서드를 통해 동작 정의
@Test
테스트 코드가 포함돼 있다고 선언하는 어노테이션
JUnit Jupiter에서 이 어노테이션을 감지해 테스트 계획에 포함시킴
@DisplayName
테스트에 대한 표현 정의
✔️ 슬라이스 테스트를 위해 사용할 수 있는 대표적인 어노테이션
@DataJdbcTest
, @DataJpaTest
, @DataMongoTest
, @DataRedisTest
, @JdbcTest
, @JooqTest
, @JsonTest
, @RestClientTest
, @WebFluxText
, @WebMvcText
, @WebServiceClientTest
단위 테스트를 위해서는 외부 요인을 모두 배제하도록 코드 작성
public class ProductServiceTest {
private ProductRepository productRepository = Mockito.mock(ProductRepository.class);
private ProductServiceImpl productService;
@BeforeEach
public void setUpTest() {
productService = new ProductServiceImpl(productRepository);
}
@Test
void getProductTest() {
Product givenProduct = new Product();
givenProduct.setNumber(123L);
givenProduct.setName("펜");
givenProduct.setPrice(1000);
givenProduct.setStock(1234);
Mockito.when(productRepository.findById(123L))
.thenReturn(Optional.of(givenProduct));
ProductResponseDto productResponseDto = productService.getProduct(123L);
// 값 검증
Assertions.assertEquals(productResponseDto.getNumber(), givenProduct.getNumber());
Assertions.assertEquals(productResponseDto.getName(), givenProduct.getName());
Assertions.assertEquals(productResponseDto.getPrice(), givenProduct.getPrice());
Assertions.assertEquals(productResponseDto.getStock(), givenProduct.getStock());
Mockito.verify(productRepository).findById(123L);
}
@Test
void saveProductTest() {
Mockito.when(productRepository.save(ArgumentMatchers.any(Product.class)))
.then(AdditionalAnswers.returnsFirstArg());
ProductResponseDto productResponseDto = productService.saveProduct(
new ProductDto("펜", 1000, 1234)
);
Assertions.assertEquals(productResponseDto.getName(), "펜");
Assertions.assertEquals(productResponseDto.getPrice(), 1000);
Assertions.assertEquals(productResponseDto.getStock(), 1234);
Mockito.verify(productRepository).save(ArgumentMatchers.any());
// any() : Mock 객체의 동작을 정의하거나 검증하는 단계에서 조건으로 특정 매개변수의 전달을 설정하지 않고
// 메서드의 실행만을 확인하거나 좀 더 큰 범위의 클래스 객체를 매개변수로 전달받는 등의 상황에 사용
}
}
Mock 객체를 직접 생성하지 않고 @MockBean
을 사용해 스프링 컨테이너에 Mock 객체를 주입받는 방식
@ExtendWith(SpringExtension.class)
@Import({ProductServiceImpl.class})
public class ProductServiceTest2 {
@MockBean
ProductRepository productRepository;
@Autowired
ProductService productService;
... 테스트 코드 동일 생략 ...
}
리포지토리는 개발자가 구현하는 레이어 중 가장 데이터베이스와 가까움
특히 구현하는 목적에 대해 고민하고 작성해야 함
리포지토리의 기본 메서드는 테스트 검증을 마치고 제공된 것이기에 findById()
, save()
같은 기본 메서드에 대한 테스트는 큰 의미가 없음
데이터베이스는 외부 요인에 속함
데이터베이스를 연동한 테스트는 테스트 데이터를 제거하는 코드까지 포함해 작성하는 것이 좋음
데이터베이스 연동 없이 테스트하는 것이 더 좋을 수도
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 실제 사용하는 데이터베이스로 테스트 가능
public class ProductRepositoryTestByH2 {
@Autowired
private ProductRepository productRepository;
// 데이터베이스 저장 테스트
@Test
void saveTest() {
// given
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(1000);
// when
Product savedProduct = productRepository.save(product);
// then
assertEquals(product.getName(), savedProduct.getName());
assertEquals(product.getPrice(), savedProduct.getPrice());
assertEquals(product.getStock(), savedProduct.getStock());
}
// 데이터베이스 조회 테스트
@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());
}
}
@DataJpaTest
@Transactional
포함 -> 테스트 코드 종료시 자동으로 데이터베이스 롤백 진행스프링의 모든 설정을 가져오고 빈 객체도 전체를 스캔하기 때문에 의존성 주입에 대해 고민할 필요 없이 테스트 가능 but 테스트 속도 느림
@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
assertThat(savedProduct.getNumber())
.isEqualTo(givenProduct.getNumber());
assertThat(savedProduct.getName())
.isEqualTo(givenProduct.getName());
assertThat(savedProduct.getPrice())
.isEqualTo(givenProduct.getPrice());
assertThat(savedProduct.getStock())
.isEqualTo(givenProduct.getStock());
/* read */
// when
Product selectedProduct = productRepository.findById(savedProduct.getNumber())
.orElseThrow(RuntimeException::new);
// then
assertThat(selectedProduct.getNumber())
.isEqualTo(givenProduct.getNumber());
assertThat(savedProduct.getName())
.isEqualTo(givenProduct.getName());
assertThat(savedProduct.getPrice())
.isEqualTo(givenProduct.getPrice());
assertThat(savedProduct.getStock())
.isEqualTo(givenProduct.getStock());
/* update */
// when
Product foundProduct = productRepository.findById(selectedProduct.getNumber())
.orElseThrow(RuntimeException::new);
foundProduct.setName("장난감");
Product updatedProduct = productRepository.save(foundProduct);
// then
assertEquals(updatedProduct.getName(), "장난감");
/* delete */
// when
productRepository.delete(updatedProduct);
// then
assertFalse(productRepository.findById(selectedProduct.getNumber()).isPresent());
}
}
TDD란?
Test-Driven Development의 약자. 테스트 주도 개발
반복 테스트를 이용한 소프트웨어 개발 방법론
테스트 코드를 먼저 작성 후 테스트를 통과하는 코드를 작성하는 과정을 반복
애자일 방법론 중 하나인 익스트림 프로그래밍(eXtream Programming)의 Test-First 개념에 기반을 둔, 개발 주기가 짧은 개발 프로세스. 단순한 설계 중시
애자일 소프트웨어 개발 방법론이란?
신속한 반복 작업을 통해 실제 작동 가능한 소프트웨어를 개발하는 개발 방식
신속한 개발 프로세스를 통해 수시로 변하는 고객의 요구사항에 대응해서 제공하는 서비스 가치 극대화
✒️ 항상 어떤 프로젝트를 개발하더라도 항상 급급하게 개발 작업에만 치중하느라, 테스트 코드를 일일이 작성하여 검증하는 일은 잘 없었던 것 같다.. 그러나 현업의 대부분은 테스트 코드 작성이며, 애플리케이션 개발에서 매우 중요한 부분이라고 하니 테스트 코드 작성 방법도 더 열심히 공부를 해야할 것 같다..!!
[출처] 스프링 부트 핵심 가이드 / 장정우, 위키북스