Rest API Controller 생성 및 단위 테스트
[목표] ➤ 테스트 코드 작성 / 단위 테스트 실행
TDD ? Unit Test?
- TDD : Test-Driven-Development
- 테스트 코드 먼저 작성하면서 시작 (Red - Green - Refactor)
- Unit Test : 기능단위의 테스트 코드 작성 (TDD의 첫번째 단계) 순수한 테스트 코드 작성 행위
- 단위 테스트는 문제 발견이나, 추후 리팩토링 같은 수정사항 발생 시 기존 기능 올바르게 작동하는지 확인 도움
- 중간중간 체크함으로써 불확실성 줄여준다. (+ 시스템에 대한 문서로 사용 가능)
- 매번 일일히 tomcat 올려서 출력 체크하는 것도 일이니까... 결과만 도출되는 단위테스트 이용하면 빠름
프레임워크 : xUnit
- 테스트 코드 작성 돕는 프레임 워크. 개발 환경에 따라 x 달라짐 (여기선 JUnit)
Hello Controller 테스트 코드 작성
1. 패키지 생성 및 java Class 생성
- java 디렉토리 하위에 생성
- 패키지 명: com.jojoldu.admin (➝ 웹사이트 주소: admin.jojoldu.com - 주로 웹사이트 주소 역순으로 생성)
- 생성된 패키지 하위에 Application 클래스 생성
- Applicaiton Class : 앞으로 만들 프로젝트의 메인 클래스
- @SpringBootApplication
- 스프링 부트 자동설정, Bean 읽기/생성 모두 자동 설정
- @SpringBootApplication이 있는 위치부터 설정 읽음!!! ➔ 항상 프로젝트 최상단에 위치하도록 주의
- SpringApplicaiton.run
- 내장 WAS(웹 애플리케이션 서버) 실행 (권장)
- 외부에 별도로 두지 않고, 어플리케이션 실행 시 내부에서 실행
- 항상 서버에 톰캣 설치할 필요 X, jar 파일로 실행하면 ok (톰캣 없이도 배포, 서비스 가능!)
- 항상 같은 환경에서 스프링부트 배포 가능하게 함
- 내장 WAS(웹 애플리케이션 서버) 실행 (권장)
2. Web 패키지 생성
- 하위 패키지로 web package 생성
- web package : Controller 관련 모든 Class 담는 패키지
3. 테스트 할 Controller 생성 [HelloController]
- HelloController 생성 : web에 테스트용 컨트롤러 생성
- @RestController Annotation
- JSON 반환 Controller로 만들어줌
- @GetMapping Annotation
- 사용자가 /hello 입력 시 문자열 hello (탬플릿 명) 반환함
4. 테스트 코드 생성 및 실행 (결과 확인)
@Autowired
private MockMvc mvc; // 스프링 MVC 테스트 시작점 / Http method API 테스트
@Test
public void hello가_리턴된다() throws Exception {
String hello = "hello";
mvc.perform(get("/hello")) //"/hello" 주소로 HTTP GET 요청 (MockMvc method)
.andExpect(status().isOk()) //HTTP Header의 Status 검증 (200 / 그 외)
.andExpect(content().string(hello)); //응답 본문 내용 검증
//체이닝--> 여러 검증 한번에 선언
}
- @ExtendWith(SpringExtension.class)
- 스프링부트 테스트 --- JUnit 사이 연결자 역할 (내장 실행자 외 다른 스프링 실행자인 SpringExtension 실행)
- @WebMvcTest
- Web(Spring MVC)에 집중 - @Controller, @ControllerAdvice... 등 사용 가능 (= 컨트롤러 클래스 주입 받기 가능)
- 대신 Web 집중 == @Service, @Component, @Repsoitory 등은 사용 불가능
- 통합 테스트를 하기 위해선 @SpringBootTest를 사용한다.
- @Autowired
- 스프링 컨테이너에서 관리하는 빈 주입받음 (DI) ; 필요한 의존 객체의 타입에 해당하는 빈을 찾아 주입
- 주로 Constructor, setter, 필드 3가지 경우에 Autowired 어노테이션 사용함
- 여기선 테스트 하기 위해 사용하는 MockMvc를 주입 받기위해 사용됨
- @Test
- Test 어노테이션을 추가해줌으로써, 하위 메소드를 test 메소드로 인식하고 테스트 함
- 즉 테스트 만드는 모듈 역할. 테스트 결과로 pass/fail 도출함
✅ MockMvc ?
⚬ 등장 배경 : 객체를 테스트 하기위해 세워놓은 허수아비 같은 느낌. 실제로 테스트하려면 메모리에 올려야 하는 데 전부 다 올리자니 부담이 크고... 내가 체크하고자 하는 기능만 가지도록 비슷하되 가벼운 객체를 만들어 모킹함.
⚬ MockMvc
- 서블릿 컨테이너를 모킹한 객체. request를 발생시키는 도구 사용, 직접 호출 건너뛰기 가능 (컨트롤러 호출해줌)
⚬ 서블릿 컨테이너 : 서블릿들의 생성, 실행, 파괴 담당 (Client의 Request 받고 Response 가능하게 웹 서버와
소켓 만들어서 통신함 EX. 톰켓)
⚬ 서블릿
- 웹 어플리케이션에서 사용자의 요청 처리 → 결과 사용자에게 다시 전송하는 일종의 프로그래밍 기술
- Servlet 클래스의 구현 규칙 따름 (ex. 서블릿 라이프 사이클 위해 필수 메소드 3가지 정의 필요)
(필수 메소드 3가지 : (1) init, (2) service, (3) destroy ... 말 그대로 생성, 실행, 파괴)
- 주 기능 : (1) HTTP Request(요청) 받아 처리 (2) Request에 대한 Response(응답)
(톰켓 방식과 같이 사용자에게 request 들어오면 컨테이너에 객체 만들어 올리고 동적 페이지 생성 후 응답
다시 내부 객체에 응답 보냄 → http 응답 사용자에게 전송)
- HTML 변경 시 재컴파일 필요함 (이래서 테스트 코드를 적극 활용하는 건가?)
# 테스트 결과가 맞는지 직접 실행해서 확인해보자
hello가 잘 return된다. 테스트가 맞다면 맞는걸로^^
롬복 설치 및 적용하기
# bulid.gradle의 dependencies 수정 (추가)
gradle 버전 5 이상부터는 의존성 추가 방식이 아래와 같이 수정됐다.
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
- lombok 플러그인은 이미 설치된 상태여서 별도의 설치는 생략했다.
✅ 이때 롬복은 프로젝트마다 설정 필요 ⇨ 새로운 프로젝트를 시작할 때마다, build.gradle 에 라이브러리 추가 + Enable annotation ~ 설정 체크는 계속 해줘야 한다.
✍ 패키지 가져오기(import) 단축키 : Alt + Enter
# 롬복으로 리팩토링 하기
Dto Package
- 모든 응답 Dto → Dto 패키지에 추가할 예정
- web package 하위에 dto package 생성 → HelloResponseDto 생성
@Getter
- 하위에 선언된 모든 필드 → get 메소드 자동으로 생성해줌 (private이므로 getter/setter 설정 필수)
@RequiredArgsConstructor
- 하위에 선언된 모든 final 필드가 포함된 생성자 생성 (final 없으면 XX)
Dto 테스트 해보기
- 생성한 HelloResponseDto에 롬복 작동 확인용 테스트 코드 작성
- main과 동일한 경로에 동일한 패키지 → HelloResponseDtoTest 생성
✅ 테스트용 코드 작성법 : (1) given (2) when (3) then
⚬ given : 검증할 때 사용할 데이터 (비교 당할 데이터)
⚬ when : 검증할 사항 (제대로 작동하는지를 검증할 메서드는 어떤 것?)
⚬ then : 검증 부분 (어떤 방식으로 검증할 것인지?)
public class HelloResponseDtoTest {
@Test //HelloResponseDto의 getter, constructor가 잘 생성되는지 확인 위해 annotation 추가
public void 롬복_기능_테스트() {
//(1)given - 검증할 때 사용할 데이터
String name = "test";
int amount = 1000;
//final로 선언해준 필드 name, amount 를 테스트에 사용
//(2)when - 검증할 사항 (뭘 검증할건지)
HelloResponseDto dto = new HelloResponseDto(name, amount);
//롬복 적용이 잘 됐는지 - Getter, Constructor이 잘 작동하는지 확인하기 위해 given data 넣어준 객체 생성
//(3)then - 어떤 방식으로 검증?
assertThat(dto.getName()).isEqualTo(name); //getName으로 name 필드 꺼내와 맞는지 비교 (isEqualto 메서드 이용)
assertThat(dto.getAmount()).isEqualTo(amount);
}
}
- Assertion을 Static import 해줌 - Assertions 없이 바로 메서드 사용 가능하게 해줌
(근데 교재를 보니 애초에 import 자체를 Assertions에서 assertThat만 해줬던거였다.... ㅎㅎ.. 아나)
- 특이한점은 Junit의 assertThat이 아닌 assertj를 사용한 것 (추가 라이브러리 필요 없는 등 장점이 더 많다고 함...)
(자매품 : Junit의 assertEquals("Expected", "Actual") 로 비교도 가능)
⇨ isEqualTo의 인자와 일치하면 통과 ( assertThat("Actual").isEqualTo("Expected")이므로)
# assertj
- 테스트 검증 라이브러리
- Assertion을 api로 제공함
✅ Assertions
- org.assertj.core의 api
- 빌드 툴에서 빌드시, 테스트 케이스를 통과하지 않으면 다음단계로 넘어가는 것이 방지됨
⚬ assertThat(argument)
- 검증하고자 하는 대상을 argument로 받음 → 주로 .isEqual과 엮어서 사용 (같으면 pass)
- 메소드 체이닝이 지원되므로 해당 메소드 뒤 바로 .isEqual 이어서 사용 가능함
⚬ isEqualTo(argument)
- 동등 비교 메소드
- 선행 메소드 assertThat의 argument와 isEqualTo의 인자가 같으면 성공(통과)
# HelloController에 ➝ ResponseDto 추가
- @RequestParam(A) : A는 API를 호출하는 곳에서 넘겨준 값들
@GetMapping("/hello/dto")
public HelloResponseDto helloDto(@RequestParam("name") String name, @RequestParam("amount") int amount) {
//사용자가 api로 넘긴 name, amount이름으로 넘긴 파라미터를 캐치함
return new HelloResponseDto(name, amount); //객체에 넣고 생성해 return
}
# 추가된 API를 테스트하는 코드 → HelloControllerTest에 추가
@Test
public void helloDto가_리턴된다() throws Exception {
String name = "hello";
int amount = 1000;
mvc.perform(
get("/hello/dto") //"/hello/dto" 주소로 HTTP GET 요청 (MockMvc method)
.param("name", name) //요청 파라미터 - name와 비교
.param("amount", String.valueOf(amount)))
.andExpect(status().isOk())//HTTP Header의 Status 검증 (200 / 그 외)
.andExpect(jsonPath("$.name", is(name)))//JSON 응답값 중 name 필드 검증
.andExpect(jsonPath("$.amount", is(amount)));//JSON 응답값 중 amount 필드 검증
}
위 코드를 추가해준다.
잘 작동된다!
# Week. 1 공부
🐢 스프링 프레임워크 등장 배경?
- EJB : 애플리케이션 업무 로직 담당하는 자바 표준 기술 → 불편함 多 → 새로운 오픈소스 등장 : J2EE / ORM
- J2EE (EJB 컨테이너 대체) : 그렇지만 얘도 컨테이너 안에서만 동작하는 객체구조...
- ORM (EJB 빈 기술 대체, JPA 새 표준 정의)
- 프레임워크 : 애플리케이션을 구성하는 근간이 되는 골격 코드 (아키텍처) → 구조 결정
- 컨테이너 : 인스턴스 생명 주기 관리(생성/소멸 관리), 생성된 인스턴스들에게 추가적인 기능 제공
- - 스프링 컨테이너 : IoC(제어권 프레임워크에게), DI 원리 적용됨. 객체 생성, 소멸, 호출)
- 빈 : 컨테이너에 의해 관리되는 인스턴스
🐢 스프링 vs. 그 외 백엔드 프레임 워크
- 스프링의 지향점이자 특이점 : 기술적 코드 / 비즈니스 코드 분리 + 우수한 객체지향 설계
⚬ 좋은 객체 지향
- 객체 지향 특징 (추상화, 캡슐화, 상속, 다형성)을 보장하며 구현의 유연성을 높인다.
- IoC, DI도 결과적으론 위와 같은 목적에 사용되는 것 (확장 가능한 설계, 선 역할 부여 후 객체 생성 → 이것들을 실현하기 위해 객체들 간 느슨한 결합이 요구되는데, 이때 DI가 사용됨)
→ SOLID(좋은 객체 지향 설계 5원칙)
1. 단일 책임 원칙, 2. 개방-폐쇄 원칙(OCP), 3. 리스코프치환 원칙, 4. 인터페이스 분리 원칙, 5. DIP(DI 역전)
🐢 스프링과 스프링 부트의 차이?
- Dependency 설정 난이도 : Spring < Spring boot
(의존관계 설정이 더 쉽고 버전 관리도 자동화. 코드 몇 줄짜리가 한 줄에 끝나는 경우도 있어 매우 간단함)
- 비슷하게 Configuration의 설정 난이도 : Spring < Spring boot
(모든 어노테이션, 빈 등록 설정 << application.properties, application.yml에 설정)
- boot에만 있는 AutoConfiguration (→ @SpringBootApplication) : 외부 라이브러리, 내장 톰켓 서버 실행해줌
(이를테면 어노테이션들을 스캔에 @Component, @Controller같은 객체들은 자동으로 Bean에 등록)
- 배포의 용이함 : Spring (war 파일을 WAS에 담아 배포) < Spring boot (jar 파일을 넣은 뒤 java -jar로 실행)
(앞서 말했듯 Boot 프레임워크는 톰캣과 같은 내장 WAS 가져서 jar로 배포해도 ㄱㅊ)
🐢 테스트 코드가 중요한 이유
- 개발한 기능 테스트 : 자바의 main 메서드 or 웹 어플리케이션 컨트롤러 통해 해당 기능 실행하며 테스트
→ 실행 소요시간 큼 + 반복 실행 어려움 + 여러 테스트 한번에 실행 어려움
➔ JUnit / assertj로 테스트 코드 만들어서 테스트 코드 자체를 실행함으로써 해결 가능
- 심지어 평소 과제할 때도 중간마다 system.out 메소드로 체크하는걸 생각해보면 당연히 중요한거 같기도...
- 물론 추후 리팩토링, 유지보수 단계에서도 유용하게 사용될 듯 (부담이 적으니)
+ 테스트를 작성할 때는 서로 의존관계 없이 설계 돼야 함 (단위 테스트니까...)
생각보다 쓸게 많구먼.... 과정을 기록해 두면 좋을 것 같아서 그렇게 했는데 별로 효율적인 방법은 아닌 것 같다.
다른 기록방법을 모색해봐야 될 것 같음..ㅎㅎ......
'Spring > [dsc] Spring-Novice-Study' 카테고리의 다른 글
[Chap3] JPA로 데이터베이스 다뤄보기 (2) | 2024.01.09 |
---|---|
[Chap_1] 프로젝트 생성 및 깃 동기화 (1) | 2022.10.01 |