테스트 코드를 먼저 작성 → 작성한 테스트 코드를 통과하게끔 실제 코드 작성하는 개발 방법
TDD 주요 원칙
테스트 실패 확인
실제 코드를 구현하기 전 테스트 케이스를 작성함
요구사항을 테스트로 변환함으로써 명확한 목표 설정 가능!
테스트 통과 가능 최소 코드 작성
테스트 통과에 꼭 필요한 최소한의 코드만 작성함
YAGNI (You Aren’t Gonna Need It) 원칙 적용
코드 개선 리펙토링
테스트 통과한 최소 코드를 개선함
중복 제거, 가독성 향상을 목표 ⇒ 설계가 개선되고 유지보수성이 증가함!
1-2. 테스트 유형
1-2-1. 단위 테스트
메서드, 클래스와 같이 개별 코드 단위를 검증하는 테스트
격리된 환경에서 실행
1-2-2. 통합 테스트
여러 단위 (메서드, 클래스)를 조합하여 상호작용을 검증하는 테스트
모듈 간 인터페이스 테스트
1-2-3. 시스템 테스트
전체 시스템의 end-to-end 기능을 검증하는 테스트
비기능적 요구사항 (성능, 보안, 사용성 등) 테스트함 ex) 스트레스 테스트, 부하 테스트 등
1-2-4. 인수 테스트
최종 사용자 관점에서 검증하는 테스트
비즈니스 요구사항이 충족되는지 여부를 확인함
2. JUnit 5
JUnit : 자바를 위한 대표적인 단위 테스팅 프레임워크
스프링 부트 버전 2.2 이상 → 자동으로 JUnit 5 의존성 추가됨
2-1. 테스트 메서드
@Test 어노테이션 붙여야 함
접근제어자 : private 이면 안됨!
반환 타입 : void 여야 함!
메서드는 파라미터를 받지 않아야 함
테스트가 Assertion 예외를 발생시키지 않으면 성공!
2-2. JUnit 테스트의 생명주기
2-2-1. 테스트 진행 과정
테스트 클래스 인스턴스를 새로 생성함
생성한 인스턴스가 가지고 있는 테스트 환경준비 (setup) 메서드를 모두 찾아 호출함
테스트 환경준비 (setup) 메서드 : 각 테스트가 실행되기 전에 호출되는 메서드 ⇒ 테스트에 필요한 초기 설정 가능!
테스트 메서드 호출함
클래스 인스턴스가 가지고 있는 테스트 환경정리 (teardown) 메서드를 모두 찾아 호출함
테스트 환경정리 (teardown) 메서드 : 각 테스트가 종료된 후에 호출되는 메서드 ⇒ 테스트로 사용한 리소스 해제하거나 데이터 삭제 가능!
2-2-2. 생명 주기 어노테이션
@Test : 실제 테스트 케이스 정리
@BeforeEach : 각 테스트 메서드가 실행되기 전에 호츌
@AfterEach : 각 테스트 메서드가 실행된 후에 호출
@BeforeAll : 테스트 클래스 (모든 테스트 메서드) 가 실행되기 전에 호출 / static 으로 선언해야 함!
@AfterAll : 테스트 클래스 (모든 테스트 메서드) 가 실행된 후에 호출 / static 으로 선언해야 함!
코드로 확인
public class JUnitCycleTest {
@BeforeAll
public static void beforeAll() {
System.out.println("JUnitCycleTest.beforeAll");
}
@BeforeEach
public void beforeEach() {
System.out.println("JUnitCycleTest.beforeEach");
}
@AfterAll
public static void afterAll() {
System.out.println("JUnitCycleTest.afterAll");
}
@AfterEach
public void afterEach() {
System.out.println("JUnitCycleTest.afterEach");
}
@Test
public void test1() {
System.out.println("JUnitCycleTest.test1");
}
@Test
public void test2() {
System.out.println("JUnitCycleTest.test2");
}
@Test
public void test3() {
System.out.println("JUnitCycleTest.test3");
}
}
/* 테스트 실행 결과
JUnitCycleTest.beforeAll
JUnitCycleTest.beforeEach
JUnitCycleTest.test1
JUnitCycleTest.afterEach
JUnitCycleTest.beforeEach
JUnitCycleTest.test2
JUnitCycleTest.afterEach
JUnitCycleTest.beforeEach
JUnitCycleTest.test3
JUnitCycleTest.afterEach
JUnitCycleTest.afterAll
*/
2-3. Assertions Method
테스트 코드 내에서 특정 조건이 참인지를 확인하는 메서드 ⇒ 테스트 케이스의 실행 결과가 예상된 결과와 일치하는 지 확인 가능!
대표적으로 두 라이브러리 존재
Junit (Assertions) : JUnit 5에서 제공하는 기본 assertion 메서드
AssertJ (Assertions) : AssertJ 라이브러리에서 제공하는 assertion 메서드
2-3-1. Junit (Assertions)
아래 import 문으로 메서드 가져옴
import static org.junit.jupiter.api.Assertions.*;
대표적인 메서드
assertEquals(기댓값, 실제값) : 기댓값과 실제값이 일치하면 통과
assertNotEquals(기대x값, 실제값) : 기대x값과 실제값이 일치하지 않으면 통과
assertTrue(조건) : 조건이 참이면 통과
assertFalse(조건) : 조건이 거짓이면 통과
assertNull(Object obj) : 객체가 null 이면 통과
assertNotNull(Object obj) : 객체가 null 이 아니면 통과
assertSame(기댓값, 실제값) : 기댓값과 실제값이 동일한 객체를 참조하면 통과
assertNotSame(기대x값, 실제값) : 기대x값과 실제값이 동일한 객체를 참조하지 않으면 통과
assertArrayEquals(기대 array, 실제 array) : 기대 array와 실제 array의 요소 순서와 값이 모두 동일하면 통과
assertThrows(기대 예외, executable) : executable이 기대한 exception을 발생시키면 통과
2-3-2. AssertJ (Assertions)
아래 import 문으로 메서드 가져옴
import static org.assertj.core.api.Assertions.*;
대표적인 메서드 (마지막 메서드 제외하고 assertThat(실제값) 뒤에 메서드 체이닝 형식으로 붙임)
.isEqualTo(기댓값) : 실제값이 기댓값과 같으면 통과
.isNotEqualTo(기대x값) : 실제값이 기대x값과 다르면 통과
.isTrue() : 조건 이 참이면 통과
.isFalse() : 조건이 거짓이면 통과
.isNull() : 객체가 null이면 통과
.isNotNull() : 객체가 null이 아니면 통과
.isSameAs(기댓값) : 실제값과 기댓값이 동일한 객체를 참조하면 통과
.isNotSameAs(기대x값) : 실제값과 기대x값이 동일한 객체를 참조하지 않으면 통과
.containsExactly(기대 array) : 배열이 기대 array와 요소 순서와 값이 모두 동일하면 통과
assertThatThrownBy(() -> executable).isInstanceOf(기대 예외) : executable이 기대한 예외를 발생시키면 통과
3. given-when-then 패턴
테스트 구조를 명확하게 나타낼 수 있음
Given
테스트에서 전제 조건을 설정하는 단계 ex) 테스트에 필요한 변수, 객체 등의 생성 등
When
실제 테스트하려는 로직을 실행하는 단계
Then
테스트 결과를 검증하는 단계
기댓값과 실제값이 일치하는지 비교 → 테스트 성공 여부 판단
예시 코드
@Test
public void sumTest(){
//given
int a = 1;
int b = 2;
//when
int result = a+b;
//then
Assertions.assertEquals(a+b, result);
}
4. 단위 테스트와 Mockito
단위 테스트
개별 모듈이나 메서드 등의 동작을 확인하는 테스트
격리된 환경에서 진행하며 의존성을 최소화함!
Mockito
테스트를 위해 사용되는 Mocking 프레임워크
(주로 단위 테스트에서) 의존성 객체를 모의(=가짜)로 만들어서 실제 객체를 대체 ⇒ 테스트 수행하도록 도와줌!
import static org.mockito.Mockito.*;
📌 mock 객체
: mockito로 생성한 모의 객체 (실제 객체의 행동을 모방)
주요 기능
Mock (모킹) : 실제 객체를 모방하는 모의 객체를 생성해서 객체의 행동을 모방함
Stub (스텁) : 실제 메서드를 호출하는 것이 아닌 미리 정의해 둔 값을 반환하도록 설정
verification (검증) : 특정 메서드가 호출되었는지 혹은 몇 번 호출되었는지 검증 가능
chaining (체이닝) : 메서드 호출 체이닝 가능
4-1. (단위 테스트) 직접 mock 객체 생성, 주입
mock(mocking할 클래스) : mocking할 클래스에 대해 mock 객체 생성
public class MemberServiceTest {
@Test
@DisplayName("Mock 예제 테스트")
public void testService() {
//given
// 의존성 객체를 모킹
MemberRepository memberRepository = mock(MemberRepository.class);
// Member 엔티티 생성
String name = "홍길동";
int age = 20;
Member member = Member.builder().name(name).age(age).build();
// 메서드 스텁 설정 -> 임의로 생성한 member 리턴!
when(memberRepository.findById(1L)).thenReturn(Optional.ofNullable(member));
// 테스트할 객체 생성 -> 모킹한 의존성 객체 주입
MemberService memberService = new MemberService(memberRepository);
//when
// 메서드 호출
Member foundMember = memberService.findById(1L);
// then
// 결과 검증 -> JUnit
assertEquals(name, foundMember.getName());
// 결과 검증 -> AssertJ
assertThat(foundMember.getAge()).isEqualTo(age);
// 메서드 호출 검증
verify(memberRepository, times(1)).findById(1L);
}
}
4-2. (단위 테스트) 어노테이션으로 자동 mock 객체 생성, 주입
@Mock : mock 객체를 자동 생성해주는 어노테이션
@InjectMocks : 인스턴스 자동 생성 + 필요한 의존관계에 대해 mock 객체를 자동 주입해줌
MockitoAnnotations.openMocks(this) : 현재 클래스에서 사용된 모든 @Mock, @InjectMocks 등의 어노테이션 처리 ⇒ mock 객체 초기화
public class MemberServiceTest {
@Mock // mock 객체 자동 생성
private MemberRepository memberRepository;
@InjectMocks // 인스턴스 생성 후 mock 객체 관련 의존관계 자동 주입
private MemberService memberService;
@BeforeEach // 각 테스트 케이스 실행 전 수행됨
void setUp() {
MockitoAnnotations.openMocks(this); // mock 객체 초기화
}
@Test
@DisplayName("Mock 예제 테스트")
public void testService() {
//given
// Member 엔티티 생성
String name = "홍길동";
int age = 20;
Member member = Member.builder().name(name).age(age).build();
// 메서드 스텁 설정 -> 임의로 생성한 member 리턴!
when(memberRepository.findById(1L)).thenReturn(Optional.ofNullable(member));
//when
// 메서드 호출
Member foundMember = memberService.findById(1L);
// then
// 결과 검증 -> JUnit
assertEquals(name, foundMember.getName());
// 결과 검증 -> AssertJ
assertThat(foundMember.getAge()).isEqualTo(age);
// 메서드 호출 검증
verify(memberRepository, times(1)).findById(1L);
}
}
5. 통합 테스트
여러 모듈 or 컴포넌트 등을 결합하여 전체 시스템의 동작을 검증하는 테스트 ⇒ 모듈 간 상호작용 및 인터페이스의 정확성 확인 가능함!
📌 단위 테스트 vs 통합 테스트
단위 테스트 : 위의 코드 예시와 같이 의존관계 최소화를 위해 mock 객체 사용, 주입 통합 테스트 : 여러 클래스, 컴포넌트들의 상호작용을 테스트하는 것 ⇒ 실제 등록된 빈등을 통해 의존관계 주입
5-1. 관련 어노테이션 정리
5-1-1. @SpringBootTest
스프링 부트에서 통합 테스트를 수행하기 위한 어노테이션
스프링 어플리케이션의 전체 context를 로드함 ⇒ 모든 빈, 설정, 컴포넌트가 실제 환경에서 동작하는 것처럼 테스트 가능함!
@SpringBootTest
class MemberApiControllerTest {
// 테스트 코드 작성
}
5-1-2. @AutoConfigureMockMvc
스프링 부트에서 제공, MockMvc 설정을 자동으로 가능하게 해줌
실제 웹 서버를 가동하지 않고도 HTTP 요청 ↔ 응답을 테스트 가능하게 함! (컨트롤러 테스트 환경)
@Autowired 를 통해 MockMvc 인스턴스 주입 가능
@SpringBootTest
@AutoConfigureMockMvc
class MemberApiControllerTest {
@Autowired
private MockMvc mockMvc;
// 테스트 코드 작성
}
5-1-3. MockMvc
웹 서버 가동 없이 스프링 MVC의 동작을 재현
HTTP 요청에 대한 시뮬레이션을 제공함
GET : get(url)
POST : post(url)
PUT : put(url)
DELETE : delete(url)
HTTP 요청 설정 메서드
.header(key, value) : 요청 헤더 설정
.contentType(타입) : 요청 본문의 타입 설정
.content(문자열) : 요청 본문의 내용 설정
.param(key, value) : 요청 파라미터 설정
HTTP 응답 검증 메서드
status() : 응답 코드 확인 메서드
.isOk(), .isCreated(), .isBadRequest() 등
header() : 응답 헤더 확인 메서드
.string(key, value) 등
jsonPath(”$.필드”).value(실제값) : JSON 응답의 필드 값이 실제값과 일치하는 지 확인
jsonPath(”$[인덱스].필드”).value(실제값) : 리스트 응답에서 해당 인덱스의 JSON 의 필드 값이 실제값과 일치하는 지 확인