TDD
1.TDD(Test Driven Development): 테스트 주도 개발
1)개요
=>소프트웨어 개발 방법론 중 하나
=>기존에 테스트 코드의 작성은 프로덕션 코드가 작성된 이후에 이루어졌지만 TDD를 적용하면 프로덕션 코드보다 실패하는 테스트 코드를 먼저 작성하고 코드를 테스트를 통과하기 위해 최소한으로 개선한 후 테스트를 통화한 프로덕션 코드를 리팩토리하는 방식
=>TDD는 테스트만을 위한 기술이 아니며 오히려 소프트웨어 설계 방법론레 가까움
=>TDD는 하나의 테스트 기술이 아니고 분석 기술이며 설계 기술
2)TDD Cycle
=>RED -> GREEN -> REFACTOR
=>RED
- 테스트 코드 작성
- 동작하는 프로덕션 코드가 없는 상황에서 테스트 코드를 먼저 작성하는 것
- 앞으로 작성될 프로덕션 코드가 어떤 동작을 하면 좋을 지 작성
- 요구 사항을 작성하는 것 과 유사
- 테스트 코드를 먼저 작성하기 때문에 테스트 코드가 기대하는 로직이 실행되지 않아 테스트는 실패할 것
- 핵심은 실패하는 테스트 코드를 작성하는 것
- 2개의 정수를 더해서 결과를 리턴하는 작업을 수행해야 하는 경우
@Test
void plusTest(){
//given
Calculator calculator = new Calculator();
//when
int result = calculator.plus(1, 3);
//then
assertThat(result).isEqualTo(4);
}
=>GREEN
- 테스트를 통과하는 최소 코드 작성
- 실패하는 테스트 코드를 통과하도록 만드는 최소한의 코드를 작성
- 빠르게 구현할 수 있는 깔끔한 코드가 있다면 바로 작성하면 되지만 깔끔하게 코드를 작성하는 일이 오래 걸릴 것 같아면 일단 테스트 코드를 통과하는 데에만 집중하여 코드를 작성하는데 이를 죄악이라고 표현함
- GREEN을 보기 위해서는 명백한 실제 구현을 입력하는 방법도 있지만 최대한 빨리 GREEN을 보기 위해서는 상수를 반환하는 코드를 작성하고 점진적으로 변수로 바꾸어가는 방법도 있음
- 위의 코드를 테스트를 통과하도록 작성
public class Calculator{
public int plus(int a, int b){
return 4;
}
}
=>REFACTOR
- GREEN을 통과하기 위해 저지른 죄악을 수습
- 빠르게 GREEN을 보기 위해 작성한 코드를 좋은 코드로 리팩토링
public class Calculator{
public int plus(int a, int b){
return a+b;
}
}
3)TDD 원칙
=>실패하는 단위 테스트를 작성하기 전에는 프로덕션 코드를 작성하지 않음
=>컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성
=>현재 실패하는 테스트를 통과하는 정도로만 실제 코드를 작성
4)TDD가 필요한 이유
=>변화에 대한 불안감 해소
- 프로덕션에 코드에 대한 테스트 코드가 이미 작성되어 있으므로 변화에 대한 영향력을 빠르게 파악할 수 있음
- 변경에 의한 문제를 사전에 빠르게 파악할 수 있음
- TDD의 핵심은 불안감 해소 와 용기
=>한번에 하나의 일만 집중할 수 있음
- TDD를 적용하지 않는 상황에서는 코드를 작성하다가 다른 코드에 한 눈이 팔리고 다른 모듈을 건드리게 되면
- TDD를 적용하면 프로덕션 코드를 작성하며 현재 실패한 테스트 케이스를 통과하는데에만 집중할 수 있음
=>빠른 피드백
- TDD를 적용하면 작성한 테스트 코드로부터 빠른 주기로 피드백받고 리팩토링하며 점진적으로 지속적으로 코드를 개선할 수 있기 때문에 버그를 찾는 시점이 빨라질 수 있음
- 처음부터 완벽한 설계를 하는 것에 집중하는 것 보다는 설계를 점진적으로 개선해 나가는데 집중할 수 있음
=>테스트 코드 자체가 문서가 될 수 있음
=>테스트를 나중에 작성하는 것은 귀찮은 작업
- 테스트 코드를 나중에 작성하면 프로덕션 코드에 맞춰 통과하는 테스트 케이스를 만들 수 있음
=>코드 퀄리티가 높아짐
- TDD를 기반으로 테스트 코드를 작성하면 의존성이 높은 테스트 코드를 작성하기 어렵기 때문에 모듈간 결합도를 낮추고 응집도를 높일 수 있음
2.TEST의 종류
1)테스트 대상의 범위를 기준으로 한 종류
=>Unit Test(단위 테스트)
- 가장 작은 단위의 테스트
- 함수나 메서드 단위로 테스트를 수행하게 되며 함수나 메서드 호출을 통해서 의도한 결과가 나오는지 확인하는 수준으로 테스트를 진행
- 테스트 비용이 적게 들기 때문에 테스트 피드백을 빠르게 받을 수 있음
=>Integration Test(통합 테스트)
- 모듈을 통합하는 과정에서의 호환성 등을 포함해 애플리케이션이 정상적으로 동작하는지 확인하기 위해 수행하는 테스트
- 단위 테스트는 모듈을 독립적으로 테스트하는 반면 통합 테스트는 여러 모듈을 함께 테스트해서 정상적인 로직 수행이 가능한지를 확인
- 단위 테스트는 특정 모듈에 대한 테스트만 진행하기 때문에 데이터베이스나 네트워크 같은 외부 요인들을 제거하고 진행하는데 반해 통합 테스트는 외부 요인들을 포함하고 테스트를 진행하기 때문에 애플리케이션이 온전히 동작하는지를 테스트
- 테스트를 수행할 때 마다 모든 컴포넌트가 동작해야 하기 때문에 테스트 비용이 커지는 단점이 있음
=>System Test
- 시스템 테스트는 정보 시스템이 완전히 통합되어 구축된 상태에서 정보 시스템의 기능을 총체적으로 테스트 하는 것
- 통합된 각 모듈들이 원래 계획했던 대로 작동하는지 시스템의 실제 동작과 원래 의도했던 요구 사항과는 차이가 없는지 등을 판단
- 수행 시간, 파일 저장 및 처리 능력, 최대 부하, 복구 및 재시동 능력, 수작업 절차 등을 점검
- 시스템 테스트는 시스템의 내부적인 구현 방식이나 설계에 대한 지식에 관계없이 테스트를 수행하는 블랙박스 테스트의 일종
- 시스템 테스트는 모든 통합 테스트를 통과한 통합된 소프트웨어 컴포넌트들 그리고 필요한 하드웨어들과 통합된 소프트웨어 시스템 전체를 대상으로 수행
- 통합 테스트는 서로 통합된 두 소프트웨어 혹은 하드웨어 단위 사이에 서로 잘 맞지 않는 부분이 있는지를 파악하기 위해서 수행하고 시스템 테스트는 통합된 컴포넌트들 사이에 발생하는 결합과 통합된 전체 시스템내에서 발생하는 결함 모두를 발견하기 위해서 수행
=>Acceptance Test(인수 테스트)
- 프로젝트에 참여하는 사람들이 토의해서 시나리오를 만들고 개발자는 이에 의거해서 코드를 작성
- 개발자가 직접 시나리오를 제작할 수 도 있지만 다른 의사 소통 집단으로부터 시나리오를 받아 개발
- 인수 테스트는 애자일 개발 방법론에서 파생했는데 익스트림 프로그래밍(XP)에서 사용하는 용어로 시나리오가 정상적으로 동작하는지를 테스트하기 때문에 통합 테스트 와는 분류가 다름
- 익스트림 프로그래밍: 고객 요구 사항 변화에 빠르게 대응하고 고품질 소프트웨어를 빠르게 전달하기 위한 목적으로 프로그래밍 방식
실천 기법
TDD
Pair Programming: 두 명이 하나의 컴퓨터에서 코딩
CI
Refactoring
Collective Code Ownership: 공유 코드 소유
Iteration: 1~2 주 정도의 짧은 반복 주기
On Site Customer: 고객 상주
- 시나리오에서 요구하는 것은 누가 어떤 목적으로 무엇을 하는가 인데 개발을 하다보면 이런 기능은 API를 통해서 드러나게 되며 인수 테스트는 API를 확인하는 방식
- 고객이 명세한 요구사항 대로 잘 작동하는지 검증
- 실제 사용자 관점에서 테스트 하는 경우가 많기 때문에 인수 테스트는 소프트웨어 내부 코드에 관심을 가지지 않는 블랙박스 테스트
- Java에서는 RestAssured 나 MockMvc 같은 도구를 활용하여 인수 테스트 가능
2)Regression Test(회귀 테스트)
=>시스템을 운영 및 유지 보수하면서 발생한 버그들을 테스트 케이스로 만들어서 이전에 발생한 버그들이 다시 발생하지 않도록 테스트 하는 것
=>버그가 발생한 입력 조건 값을 사용하여 정상 동작하는 테스트 케이스를 작성
3)Black Box Test & White Box Test
=>Black Box Test
- 내부 구조나 소스 코드에는 관심을 두지 않고 Input 과 Output에 초점을 맞추는 방식
- 테스트 대상: 사용자가 보는 기능, UI, 요구사항 충족 여부
- 종류
Equivalence Partitioning: 모든 경우를 동일한 비율로 테스트
Boundary Value Analysis: 일반적으로 경계값에서 오류가 많이 발생하므로 경계값 앞 뒤의 데이터를 가지고 테스트
Decision Table
상태 전이 테스트
- 장점
개발 지식이 없어도 가능: 테스터나 사용자도 가능
요구사항 기준으로 테스트하므로 결함 발견이 사용자 경험에 가까움
- 단점
내부 로직을 보지 않기 때문에 코드 커버리지 보장이 어렵습니다.
테스트 케이스가 요구사항에만 의존
=>White Box Test
- 프로그램의 내부 구조, 소스코드 흐름을 고려해 테스트하는 방식
- 대상: 코드의 분기, 조건, 루프 등 구현 로직 자체
- 방법
Statement Coverage
Branch Coverage
Path Coverage
- 장점
내부 동작을 상세히 검증 - 버그 발견 확률이 높음
코드의 모든 경로를 테스트 - 테스트 커버리지가 높아짐
- 단점
개발 지식 필요 - 테스터가 코드를 이해해야 함
대규모 시스템에서는 모든 경로를 테스트하는 것이 어려움
4)Alpha Test 와 Beta Test
=>Alpha Test
- 개발자 또는 사용자(내부 테스터)가 개발사 내부에서 테스트
- 외부 사용자에게 배포하기 전에 치명적 결함을 제거하기 위해서 수행
- 기본 기능이 제대로 동작하는지 확인
=>Beta Test
- 실제 사용자에게 배포해서 사용성, 호환성, 버그를 검증하는 테스트
- 블랙박스 테스트 성격이 강함
- 목적
실제 사용자 관점에서 제품 평가
예상치 못한 환경적 이슈, 사용성 문제 해결
정식 추리 시 전 시장 반응 확인
5)Static Test(정적 테스트)
=>프로그램을 실행하지 않고 산출물(코드, 문서, 설계서 등)을 검토 분석하는 테스트 기법
=>소프트웨어 실행을 통한 동작 검증 과 달리 결함을 사전에 예방하는 목적이 강함
=>주요 활동
- Review
Peer Review
WalkThrough
Inspection
- Static Analysis
코드 실행없이 도구를 이용해서 코딩 규칙, 잠재적 버그, 보안 취약점을 자동 분석
SonarQube, ESLint, FindBugs, PMD 등
=>장점
- 소프트웨어 초기 단계에서 결함 발견 - 수정 비용 절감
- 실행 전 발견 가능 - 시간 과 자원 절약
- 품질 표준 및 코딩 규칙 준수 여부 확인 가능
- 문서/코드의 가독성, 유지보수성 향상
=>단점
- 실행 결과를 직접 확인하지 않음
- 모든 오류를 찾아낼 수 없음
- 리뷰 시 참여자의 역량에 좌우
3.테스트 코드 작성 방법
1)Given-When-Then 패턴
=>단계를 설정해서 각 단계의 목적에 맞게 코드를 작성
=>단계
- Given: 테스트를 수행하기 전에 테스트에 필요한 환경을 설정하는 단계로 테스트에 필요한 변수를 정의하거나 Mock 객체를 통해 특정 상황에 대한 행동을 정의하는 과정
- When: 테스트의 목적을 보여주는 단계로 실제 테스트 코드가 포함되며 테스트를 통한 결과를 가져오게 됨
- Then: 테스트의 결과를 검증하는 단계로 When 단계에서 나온 결과를 검증하는 작업을 수행하며 결과가 아니더라도 테스트를 통해서 검증해야 한다면 이 단계에 포함
=>단위 테스트보다는 인수 테스트에서 사용하는 것을 권장
=>코드 예시
@Test
public void testHashSetContainsNonDuplicatedValue(){
//Given
Integer value = Integer.valueOf(1);
Set<Integer> set = new HashSet<>();
//When
set.add(value);
set.add(value);
set.add(value);
//Then
Assertion.assertEquals(1, set.size());
Assertion.assertTrue(set.contains(value));
}
2)좋은 테스트를 위한 5가지 속성
=>Fast
- 테스트 코드는 빠르게 동작해야 함
=>Isolation
- 하나의 테스트 코드는 목적으로 여기는 하나의 대상에 대해서 수행되어야 한다.
=>Repeatable
- 반복 가능
=>SelfValidating
- 테스트는 그 자체로 테스트의 검증이 완료되어야 함
=>Timely
- 테스트 코드는 테스트하려는 애플리케이션 코드를 구현하기 전에 완성되어 함
4.Spring Boot 테스트 설정
=>별도의 starter를 제공: spring-boot-starter-test
1)제공되는 라이브러리
- JUnit
테스트 케이스를 작성하고 실행할 수 있는 기능들을 제공
테스트 케이스를 작성할 수 있는 어노테이션 과 실행한 테스트 결과 값을 예상 값과 비교 및 검증할 수 있는 클래스들을 제공
- Hamcrest
실행한 테스트 결과 값을 예상 값과 비교 및 검증할 수 있는 클래스들을 제공
JUnit 보다는 가독성이 높음
여기에 있는 메서드들은 서술적으로 작성할 수 있기 때문
- Mockito
개발자가 입력 값에 따라 출력 값을 프로그래밍 한 일종의 가짜 객체
목 객체를 사용할 수 있는 기능을 제공
- spring-test
스프링 프레임워크의 기능을 통합 테스트 할 수 있는 기능을 제공
- spring-boot-test
스프링 부트 프레임워크의 기능을 통합 테스트 할 수 있는 기능을 제공
- sprint-boot-test-autoconfiguration
스프링 부트 프레임워크의 통합 테스트를 자동화 해주는 기능을 제공
2)JUint을 테스트
=>개요
- 자바 언어에서 사용되는 대표적인 테스트 프레임워크로서 단위 테스트 뿐 만 아니라 통합 테스트를 할 수 있는 기능을 제공
- Annotation 기반의 테스트 방식을 지원하는데 몇 개의 Annotation 만으로 간단하게 테스트 코드를 작성
=>@Test
- 테스트 코드를 포함하는 메서드를 정의할 때 사용하는 Annotation
- 메서드의 접근 지정자는 public
- 리턴 타입은 void
- 메서드 이름은 test로 시작하는 것이 일반적
=>JUnit life cycle
- @BeforeAll: 테스트 전에 한 번 만 수행
- @BeforeEach: 테스트 메서드 각각에 실행
- @AfterAll: 테스트 후에 한 번 만 수행
- @AfterEach: 테스트 메서드 개수만큼 실행
=>테스트 코드 작성
import org.junit.jupiter.api.*;
import java.util.HashSet;
import java.util.Set;
public class MiscTest {
@BeforeAll
public static void setUp(){
System.out.println("setUp");
}
@BeforeEach
public void init(){
System.out.println("init");
}
@AfterAll
public static void destroy(){
System.out.println("destroy");
}
@AfterEach
public void cleanUp(){
System.out.println("cleanUp");
}
@Test
public void testHashSetContainsNonDuplicatedValue(){
//Given
Integer value = 1;
Set<Integer> set = new HashSet<>();
//When
set.add(value);
set.add(value);
set.add(value);
//Then
Assertions.assertEquals(1, set.size());
Assertions.assertTrue(set.contains(value));
}
@Test
public void testDummy(){
Assertions.assertTrue(true);
}
}
=>Assertions 클래스
- static 검증 메서드를 제공
- 검증 메서드는 assert 라는 머리말로 시작
- 검증에 실패하는 경우 AssertionFailedError 예외가 발생하고 JUnit 프레임워크는 이 정보를 바탕으로 각 테스트 케이스의 성공 과 실패 여부를 판단
- 검증 메서드의 인자 이름이 expect 이면 예상값을 의미하고 actual 이면 테스트 대상의 실제 값을 의미
- 2개의 인자를 받는 메서드의 인자 순서는 변경이 되도 테스트를 진행할 수 있지만 순서가 변경이되면 정확한 테스트 결과 메시지를 볼 수 없음
3)@SpringBootTest
=>이 어노테이션을 클래스에 추가하면 이 클래스는 Spring Boot의 Annotation을 인지하게 됩니다.(ApplicationContext 사용이 가능)
=>내장 컨테이너를 이용해서 테스트
- 요청 DTO 클래스 생성: dto.HotelRequest
@Getter
@Setter
@ToString
public class HotelRequest {
private String hotelName;
public HotelRequest(){
}
public HotelRequest(String hotelName) {
this.hotelName = hotelName;
}
}
- 응답 DTO 클래스 생성 - dto.HotelResponse
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class HotelResponse {
private Long hotelId;
private String hotelName;
private String address;
private String phoneNumber;
public HotelResponse(Long hotelId, String hotelName, String address, String phoneNumber) {
this.hotelId = hotelId;
this.hotelName = hotelName;
this.address = address;
this.phoneNumber = phoneNumber;
}
}
=>Service 인터페이스 생성 - service.DisplayService
import com.example.demo.dto.HotelRequest;
import com.example.demo.dto.HotelResponse;
import java.util.List;
public interface DisplayService {
List<HotelResponse> getHotelsByName(HotelRequest hotelRequest);
}
=>Service 클래스 생성 - service.DisplayServiceImpl
import com.example.demo.dto.HotelRequest;
import com.example.demo.dto.HotelResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class DisplayServiceImpl implements DisplayService{
public List<HotelResponse> getHotelsByName(HotelRequest hotelRequest) {
try{
TimeUnit.SECONDS.sleep(5);
}catch(InterruptedException e){
log.error("ERROR", e);
}
return List.of(new HotelResponse(
1000L, "Ragged Point Inn", "18091 CA -1",
"01037901997"));
}
}
=>Test를 위한 클래스를 생성: DisplayServiceTest
import com.example.demo.dto.HotelRequest;
import com.example.demo.dto.HotelResponse;
import com.example.demo.service.DisplayService;
import com.example.demo.service.DisplayServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import java.util.List;
@SpringBootTest
public class DisplayServiceTest {
@Autowired
private DisplayService displayService;
@Autowired
private ApplicationContext applicationContext;
@Test
public void testDisplayService(){
//Given
HotelRequest hotelRequest = new HotelRequest("Line Hotel");
//When
List<HotelResponse> hotelResponses = displayService.getHotelsByName(hotelRequest);
//Then
Assertions.assertNotNull(hotelResponses);
Assertions.assertEquals(1,hotelResponses.size());
}
@Test
public void testApplicationContext(){
DisplayService displayService =
applicationContext.getBean(DisplayService.class);
Assertions.assertNotNull(displayService);
Assertions.assertTrue(DisplayService.class.isInstance(displayService));
}
}
=>build.gradle 파일의 dependencies 부분에 추가
compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.34'
4)TestConfiguration
=>테스트 케이스를 작성할 때는 테스트 대상 클래스의 기능을 테스트 할 수 있도록 테스트 범위를 줄이는 것이 중요
=>대상 클래스의 기능이 다른 클래스에 의존할 때는 의존하는 클래스 설정에 따라 대상 클래스의 기능을 정확하게 테스트할 수 없는 경우가 발생
=>테스트 대상 클래스 내부에 데이터베이스에 쿼리하는 기능을 포함하거나 원격 서버의 RES-API를 호출하는 기능을 포함하는 경우 테스트 환경은 Dev 또는 Production 환경 과 분리되어 있어 테스트를 위한 데이터베이스나 서버를 구축해야 하는데 서버를 구축해도 같은 테스트를 여러번 실행하면 데이터베이스의 데이터나 서버의 데이터가 훼손되서 매번 같은 결과를 얻을 수 없을 수 있음
=>테스트 환경을 구축하지 못했다면 테스트를 실행할 때마다 ConnectionException 같은 예외가 발생하여 테스트를 정상적으로 실행할 수 없으므로 테스트를 할 때 테스트 대상 클래스의 기능만 독립적으로 테스트 할 수 있으면 되는데 이 경우 가상의 실행 환경을 만들면 가능
=>스프링 프레임워크에서 제공하는 @TestConfiguration 어노테이션을 사용하면 테스트 환경을 쉽게 구축할 수 있음
=>@TestConfiguration 은 @Configuration과 비슷한 기능을 제공하며 자바 설정 클래스를 정의하는 목적으로 사용
=>@TestConfiguration이 사용된 클래스에는 @Bean 으로 정의한 스프링 빈을 포함할 수 있음
=>Test 환경 설정
- Entity 클래스 생성: domain.HotelRoomEntity
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class HotelRoomEntity {
private Long id;
private String code;
private Integer floor;
private Integer bedCount;
private Integer bathCount;
public HotelRoomEntity(Long id, String code, Integer floor, Integer bedCount, Integer bathCount) {
this.id = id;
this.code = code;
this.floor = floor;
this.bedCount = bedCount;
this.bathCount = bathCount;
}
}
- 응답하는 클래스 - domain.HotelRoomResponse
import lombok.Data;
@Data
public class HotelRoomResponse {
private Long id;
private String code;
private Integer floor;
private Integer bedCount;
private Integer bathCount;
public HotelRoomResponse(Long id, String code, Integer floor, Integer bedCount, Integer bathCount) {
this.id = id;
this.code = code;
this.floor = floor;
this.bedCount = bedCount;
this.bathCount = bathCount;
}
//Entity를 받아서 Response로 변경해주는 메서드
public static HotelRoomResponse from(HotelRoomEntity hotelRoomEntity){
return new HotelRoomResponse(hotelRoomEntity.getId(),
hotelRoomEntity.getCode(),
hotelRoomEntity.getFloor(),
hotelRoomEntity.getBedCount(),
hotelRoomEntity.getBathCount());
}
}
- 데이터베이스 연동 클래스: persistence.HotelRoomRepository
@Repository
public class HotelRoomRepository {
public HotelRoomEntity findById(Long id) {
return new HotelRoomEntity(id, "EAST-1902", 19, 2, 1);
}
}
- 서비스 클래스 생성: service.HotelRoomDisplayService
import com.example.demo.domain.HotelRoomEntity;
import com.example.demo.domain.HotelRoomResponse;
import com.example.demo.persistence.HotelRoomRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
@Slf4j
@Service
//@RequiredArgsConstructor
public class HotelRoomDisplayService {
private final HotelRoomRepository hotelRoomRepository;
private final ThreadPoolTaskExecutor threadPoolTaskExecutor;
public HotelRoomDisplayService(HotelRoomRepository hotelRoomRepository,
ThreadPoolTaskExecutor threadPoolTaskExecutor) {
this.hotelRoomRepository = hotelRoomRepository;
this.threadPoolTaskExecutor = threadPoolTaskExecutor;
}
public HotelRoomResponse getHotelRoomById(Long id){
HotelRoomEntity hotelRoomEntity = hotelRoomRepository.findById(id);
threadPoolTaskExecutor.execute(() -> {log.warn("entity:{}", hotelRoomEntity.toString());});
return HotelRoomResponse.from(hotelRoomEntity);
}
}
- Configuration 클래스 생성: config.ThreadPoolConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class ThreadPoolConfig {
@Bean(destroyMethod="shutdown")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(10);
executor.setThreadNamePrefix("AsyncExecutor-");
executor.afterPropertiesSet();
return executor;
}
}
- Test를 위한 Configuration 클래스 생성 - config.TestConfig
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@TestConfiguration
public class TestConfig {
@Bean(destroyMethod="shutdown")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(1);
executor.setThreadNamePrefix("TestExecutor-");
executor.afterPropertiesSet();
return executor;
}
}
- 2개의 설정 클래스에서 동일한 Bean을 2개 만들려고 하므로 Overriding 할 수 있도록 설정을 추가하기 위해서 resources 디렉토리에 설정 파일을 추가하고 작성: application-test.properties
spring.main.allow-bean-definition-overriding=true
- Test 클래스 생성: HotelRoomDisplayServiceTest
import com.example.demo.config.TestConfig;
import com.example.demo.domain.HotelRoomResponse;
import com.example.demo.service.HotelRoomDisplayService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
@SpringBootTest
@ContextConfiguration(classes = TestConfig.class)
@TestPropertySource(locations = "classpath:application-test.properties")
public class HotelRoomDisplayServiceTest {
@Autowired
private HotelRoomDisplayService hotelRoomDisplayService;
@Test
public void testTestConfiguration(){
HotelRoomResponse hotelRoomResponse = hotelRoomDisplayService.getHotelRoomById(1L);
Assertions.assertNotNull(hotelRoomResponse);
Assertions.assertEquals(1L, hotelRoomResponse.getId());
}
}
5)MockBean
=>스프링 부트의 기본 테스트 모듈에는 Mockito 와 @MockBean 어노테이션을 같이 제공
=>가짜 객체를 만들 수 있는 라이브러리
=>가짜 Repository를 이용하는 테스트: HotelRoomDisplayServiceTest
import com.example.demo.domain.HotelRoomEntity;
import com.example.demo.domain.HotelRoomResponse;
import com.example.demo.persistence.HotelRoomRepository;
import com.example.demo.service.HotelRoomDisplayService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
@SpringBootTest
public class HotelRoomDisplayServiceTest01 {
@Autowired
private HotelRoomDisplayService hotelRoomDisplayService;
//가짜 객체 생성
@MockitoBean
private HotelRoomRepository hotelRoomRepository;
@Test
public void testMockBean(){
given(this.hotelRoomRepository.findById(any()))
.willReturn(new HotelRoomEntity(10L, "test", 1,1,1));
HotelRoomResponse hotelRoomResponse = hotelRoomDisplayService.getHotelRoomById(1L);
Assertions.assertNotNull(hotelRoomResponse);
Assertions.assertEquals(10L, hotelRoomResponse.getId());
}
}
6)Test Slice Annotation
=>개요
- @SpringBootTest 어노테이션으로 테스트를 실행하면 ApplicationContext를 이용하여 스프링 빈을 스캔하고 의존성을 주입
- 애플리케이션의 기능이 많다면 스캔해야 할 대상이 많아지고 그 만큼 많은 객체를 생성해야하기 때문에 테스트 시간이 오래 걸릴 수 밖에 없는데 이는 F.I.R.S.T 원칙 중 Fast 항목에 위배됨
- 테스트를 진행하여 결과를 확인하는 피드백 시간이 늘어나면 애플리케이션을 디버깅 할 시간이 줄어들고 배포 전략에 따라 테스트를 통과해야 패키징을 할텐데 테스트 시간이 오래 걸리면 빠른 배포를 할 수 없음
- 스프링 부트 테스트 프레임워크에서는 테스트 시간을 줄이기 위해서 테스트 슬라이스 개념을 제공하는데 기능 별로 잘라서 테스트 대상을 줄일 수 있는 방법을 제공
- 테스트 대상이 작아지고 ApplicationContext가 스캔해야 하는 스프링 빈의 개수도 줄어들고 기능을 초기화하지 않아도 됨
=>종류
- @WebMvcTest
스프링 MVC 프레임워크의 기능을 테스트하는 것으로 @Controller, @ControllerAdvice를 스캔하고 Converter, Filter, WebMvcConfigurer 같은 기능을 제공
@Service, @Component, @Repository는 스캔하지 않음
- @DataJpaTest
JPA 기능을 테스트
@Repository 만 스캔
- @JsonTest
직렬화 관련 기능만 테스트
- @RestClientTest
클라이언트의 동작을 테스트
- @DataMongoTest
=>Controller 클래스 테스트
- Controller 클래스 생성 - HotelController
import com.example.demo.dto.HotelRequest;
import com.example.demo.dto.HotelResponse;
import com.example.demo.service.DisplayService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@RestController
@RequiredArgsConstructor
public class HotelController {
private final DisplayService displayService;
@ResponseBody
@PostMapping("/hotels/fetch-by-name")
public ResponseEntity<List<HotelResponse>> getHotelByName(@RequestBody HotelRequest hotelRequest){
List<HotelResponse> hotelResponseList = displayService.getHotelsByName(hotelRequest);
return ResponseEntity.ok(hotelResponseList);
}
}
- Json을 위한 클래스 생성 - JsonUtil
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonUtil {
public static final ObjectMapper mapper = new ObjectMapper();
}
- Test 클래스 생성: ApiControllerTest01