TDD (Test Driven Develpment)
- 테스트 주도 개발
-> 성공 테스트뿐만 아니라 실패 테스트까지 작성해야한다 - 반복 테스트를 이용한 소프트웨어 방법론
- 작은 단위의 테스트 먼저 설계 및 구축 후 테스트를 통과할 수 있는 코드를 짜는 것
- 애자일 개발 방식 중 하나
-> 코드 설계시 원하는 단계적 목표에 대해 설정하여 진행하고자 하는 것에 대한 결정 방향의 갭을 줄이고자 함
테스트 코드 작성 목적
- 코드의 안정성 높일 수 있음
- 기능을 추가하거나 변경하는 과정에서 발생할 수 있는 side-effect를 줄일 수 있음
-> A기능을 수정하기위해 어떤 메소드를 수정하는데, 이 메소드를 B기능에서도 사용하고있었을 때. B기능이 동작이 제대로 되지않을 수 있는데 이와 관련된 테스트코드 미리 작성해뒀다면 이를 이용해 side-effect방지가능 - 해당 코드가 작성된 목적을 명확히 표현할 수 있음
-> 코드에 불필요한 내용이 들어가는 것을 줄일 수 있음
필요 라이브러리
Java 단위테스트 작성에는 크게 2가지 라이브러리가 사용됨
- Junit : 자바 단위 테스트를 위한 테스팅 프레임워크
- AssertJ : 자바 테스트를 돕기 위해 다양한 문법을 지원하는 라이브러리
단위테스트
- 코드의 특정 모듈이 의도된 대로 동작하는지 테스트하는 절차
- 모든 함수와 메소드에 대한 각각의 테스트 케이스 작성하는 것
FIRST 원칙
- Fast : 테스트는 빠르게 동작해 자주 돌릴 수 있어야한다.
-> 테스트가 느리면 개발자가 테스트를 주저하게될 것
-> 자주 검증하지 않은 소스코드는 버그가 발생활 확률 높아진다. - Independent : 독립적인 테스트가 가능해야함
-> 테스트에 필요한 데이터는 테스트 내부에서 독립적으로 사용해야 함
-> 데이터 존재 여부를 찾는 테스트가 있는 경우엔 해당 데이터는 테스트 내부에서 생성되어야 하며, 나중에 다른데 영향 미치지않게 제거해야함 - Repeatable : 어느 환경에서도 반복가능해야하고, 매번 같은 결과를 만들어야 함
-> 네트워크나 데이터베이스에 의존하지 않는 환경
-> 환경에 의존하지 않는 테스트가 실패할 수 있는 이유는 오로지 테스트할 클래스 또는 메소드가 제대로 작동하지 않기때문 - Self-Validating : 테스트는 그 자체로 실행하여 결과 확인 가능해야함
-> 테스트가 실행될 때마다 메서드 출력이 올바른지 확인하는 것은 개발자가 결정해서는 안됨 - Timely : 비즈니스 코드가 완성되기 전에 구성하고 테스트가 가능해야 함
Junit
- 단위 테스트(Unit Test)를 위한 도구 제공
- 어노테이션 기반 테스트 지원
- 단정문(Assert)으로 테스트 케이스의 기대값에 대해 수행결과 확인 가능
- Spring Boot 2.2버전부터 Junit 5버전 사용
- Junit5는 크게 Jupiter,Platform,Vintage모듈로 구성됨
모듈 설명
JUnit Jupiter
- TestEngine API구현체로 JUnit 5구현하고있음
- 테스트의 실제 구현체는 별도 모듈 역할을 수행하는데, 그 모듈 중 하나가 Jupiter-Engine
- 이 모듈은 Jupiter-API를 사용해 작성한 테스트 코드를 발견하고 실행하는 역할을 수행
- 개발자가 테스트 코드 작성할 때 사용됨
JUnit Platform
- Test 실행하기 위한 뼈대
- Test를 발견하고 테스트 계획을 생성하는 TestEngine 인터페이스 갖고있음
- TestEngine통해 Test발견하고 수행 및 결과 보고
JUnit Vintage
- TestEngine API구현체로 JUnit 3,4를 구현하고 있음
- 기존 JUnit 3,4 버전으로 테스트 코드 실행할 때 사용됨
- Vintage-Engine모듈 포함하고있음
JUnit LifeCycle Annotation
JUnit 5기준
JUnit Main Annotaion
@SpringBootTest
- 통합 테스트 용도
- @SpringBootApplication을 찾아가 하위의 모든 Bean 스캔해 로드함
- 그 후 Test용 Application Context를 만들어 Bean을 추가하고, 만약 MockBean으로 추가된게 있다면 해당 Bean을 MockBean으로 교체
@ExtendWith
- JUnit4에서 @RunWith로 사용되던 어노테이션이 ExtendWith로 변경됨
- @ExtendWith는 메인으로 실행될 Class를 지정할 수 있음
- Mockito의 Mock 객체를 사용하기 위한 Annotation
- @SpringBootTest는 기본적으로 @ExtendWith가 추가되어 있음
@WebMvcTest(Class명.class)
- ()에 작성된 클래스만 실제로 로드하여 테스트 진행
- 매개변수를 지정하지않으면 @Controller, @RestController, @RestControllerAdvice 등 컨트롤러와 연관된 Bean 모두 로드됨
- 스프링의 모든 Bean을 로드하는 @SpringBootTest 대신 컨트롤러 관련 코드만 테스트할 경우 사용
'@Autowired' about Mockbean
- Controller의 API를 테스트하는 용도인 MockMvc 객체를 주입받음
-> MockMvc 객체 선언하고 그 위에 @Autowired 선언해야함 - perform() 메소드를 활용해 컨트롤러의 동작 확인가능
-> andExpect(), andDo(), andReturn() 등 메소드를 같이 활용함
@MockBean
- 테스트할 클래스에서 주입 받고 있는 객체에 대해 가짜 객체를 생성해주는 어노테이션
- 해당 객체는 실제 행위를 하지않음
- given() 을 활용해 가짜 객체의 동작에 대해 정의하여 사용가능
@AutoConfigureMockMvc
- spring.test.mockmvc의 설정을 로드하면서 MockMvc의 의존성 자동주입
- MockMvc클래스는 REST API 테스트를 할 수 있는 클래스
@Import
- 필요한 Class들을 Configuration으로 만들어 사용가능
- Configuration Component클래스도 의존성 설정가능
- Import된 클래스는 주입으로 사용 가능
Spring Boot 프로젝트의 테스트 코드
프로젝트를 열어보면 기본적으로 src- main, src-test 폴더구조가 생성되어있는데, main과 test의 구조가 동일한것을 볼 수 있다.
-> 테스트하기 위해서는 이후 생성하는 파일들도 main과 경로를 맞춰줘야한다!
test에 있는 '${프로젝트명}ApplicationTests'파일을 보면 기본적으로 아래와같이 코드가 생성되어있다.
@SpringBootTest
class SeatSenceApplicationTests {
@Test
void contextLoads() {
}
}
@Test가 붙어있는 void contextLoads()는 뭘까?
-> @SpringBootTest 통합테스트 어노테이션이 붙어있기때문에, 모든 컨텍스트가 로드되어있는지 테스트해보는. 기본적으로 생성되어있는 테스트이다.
테스트 코드는 아니지만, 동작을 보기위해 기본적인 어노테이션들을 살펴보자.
기본동작
package test;
import org.junit.jupiter.api.*;
public class TestLifeCycle {
@BeforeAll
static void beforeAll() {
System.out.println("## BeforeAll Annotation 호출 ##");
System.out.println();
}
@AfterAll
static void afterAll() {
System.out.println("## BeforeAll 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 // Disabled Annotation : 테스트를 실행하지 않게 설정
void test3() {
System.out.println("## test3 시작 ##");
System.out.println();
}
}
위와같은 결과를 볼 수 있다.
커서가 눌린 두번째 줄은 클래스명이고, 그 아래는 Test되는 메소드의 이름이 뜬다.
-> DisplayName으로 이름을 설정해뒀으면 해당 이름이 뜬다.
test3의 옆에있는 표시는 테스트가 수행되지 않았음을 뜻한다. (Disabled 설정)
동작 순서는 위 이미지와 같다.
Spring Boot 프로젝트의 Controller 테스트 코드
ProductController.class파일을 테스트하기위한 'ProductControllerTest' 소스파일을 생성한다.
@MockBean으로 ProductServiceImpl은 왜 생성하는걸까?
ProductController 코드를 보면, ProductService를 Autowired해서 주입받고있다.
따라서 ProductServiceImpl에 대한 Mock 객체를 생성하는것이다.
이어서 테스트 코드를 계속보자.
given() : Mock객체가 특정 상황에서 해야하는 행위를 정의하는 메소드. 어떤 상황이 주어진다고 생각
-> mokito 라이브러리에서 가져온 메소드
-> mokito : mock 객체를 생성, 사용하는데 도와주는 라이브러리
-> MockBean으로 만든 Mock객체 (productService) 사용
- getProduct() : productDto객체를 return해주는 메소드
-> 따라서 코드에서보면 getProduct.willlReturn의 파라미터에 new ProductDto로 객체를 생성하고있음 - willReturn() : 어떠한 값이 넘어올거야
andExpect() : 기대하는 값이 나왔는지 체크해볼 수 있는 메소드
-> Builder구조이기때문에, .을 구분해서 사용함
- perform() : REST API 테스트할 수 있는 환경 만들어줌
- get() : 실제로 우리가 어떤 통신을 할건지에대해 정의해줌. get이라는 Http Request 날릴 예정
-> 파라미터 : 경로 (로컬이기때문에, /api 앞부분은 (포트번호까지) 제외가능 - status().isOk() : status가 'ok'로 나왔는지 확인
- jsonPath() : Http Request를 날리면 기본적으로 json형태의 바디값을 받기때문에, 파라미터에 있는 값들이
-> exists() : 존재하는지 확인 - andDo() : 요청에 대한 처리
-> print() : 위에서 테스트한 내용을 출력
verify() : 해당 객체의 메소드가 실행되었는지 체크해줌
위 코드상에서는 productService라는 객체에서 "12315"로 getProduct가 실행되었는지 체크
그 다음 'createProductTest' 코드를 보자.
given()을 이용해서 saveProduct()가 호출된다면 가정해주고, willReturn()을 리턴해주는지 사전 세팅
post 통신을 할것이기때문에, dto객체를 먼저 만들어준다.
ProductDto.builder()..
- Gson : Google에서 만든 Json의 형태를 자유롭게 다룰 수 있게 편의를 제공하는 라이브러리
-> toJson(productDto) : productDto값을 Json으로 변경 - post() : 파라미터에 경로지정
-> .content() : 어떤 바디값을 넘겨줄건지
-> .contentType() :
verify()
-> saveProduct() 파라미터에 들어가는 객체가 saveProduct로 실행됐는지 검증
Spring Boot 프로젝트의 Service 테스트 코드
- 내가 어떤 객체를 가져올지 모르겠으면 @SpringBootTest() 사용해 {}안에 묶어서 실행
-> 매개변수 없으면 전체 Bean로드 - @ExtendWith, @Import 방식
@ExtendWith는 @SpringBootTest에 포함되어있으므로, 필요한 내용만 갖다쓴 것 - @MockBean으로 Handler생성은 뭘까?
Service의 코드를 보자.
Handler라는 객체를 주입받아 사용중이다.
따라서 Test코드에서 Mock객체로 생성해서 사용해야한다. - @Autowired로 ServiceImpl?
Controller테스트가 아니기때문에 (WebMvc 테스트가 아니기때문에), 테스트하고자 하는 객체를 주입받아 사용해야한다. - given : Mokito.when() 이런상황일 때, thenReturn() 이걸 리턴해줘라
(위에 잘린부분 이어서)
Service의 또 다른 테스트
'Study > 개발일지' 카테고리의 다른 글
[백엔드TIL] AWS serverless 개발 -cognito 개념 (0) | 2023.10.20 |
---|---|
[백엔드 TIL] WEB 과 WAS 개념 (1) | 2023.10.18 |
[백엔드TIL] Java Comparable과 Comparator의 차이 (1) | 2023.10.11 |
[백엔드TIL] 자바와 코틀린의 차이 (0) | 2023.10.10 |
[백엔드TIL] Spring Batch에 대한 이해 (1) | 2023.10.06 |