『스프링 부트와 AWS로 혼자 구현하는 웹 서비스』 도서를 읽다 작성한 글입니다.
점차 많은 서비스 회사에서 테스트 코드를 요구하고 있다. (채용 정보의 우대사항에서도 볼 수 있다.)
이번 포스팅의 목차는 다음과 같다.
- 테스트 코드의 필요성(장점)
- 테스트 코드 프레임워크 종류
- 테스트 코드 작성(기초)
- 간단한 Controller 테스트
- 필자가 만난 오류 및 해결방법
- 롬복 테스트 코드 (2개)
- 간단한 Controller 테스트
테스트 코드의 필요성(장점)
많은 장점이 있지만 두드러지는 장점은
1. 코드를 작성한 후, 서버를 실행해서 확인하는 귀찮은 일을 반복할 필요가 없다는 것이다.
톰캣을 재시작하는 것은 생각보다 오랜 시간이 걸린다..
2. print문으로 확인하는 수동적인 검증을 할 필요 없다.
자동 검증이 가능하다.
또한 기존에 많은 기능이 있을 때, 새 기능을 추가하면서 문제가 생기는 경우가 빈번히 발생할 수 있다.
그렇다고 하나의 기능을 추가할 때마다 모든 기능을 테스트할 수는 없다.
3. 이를 위해 기존 기능이 잘 작동되도록 보장하는 것 또한 테스트코드의 장점이다.
서비스 기업에서 특히나 강조되고 있는 테스트 코드는 개발자가 익혀야할 기술이자 습관이다. 이를 습관화할 수 있도록 하자!
테스트코드 프레임워크
가장 대중적인 테스트 프레임워크는 xUnit이다.
이는 개발환경(x)에 따라 Unit 테스트를 도와주는 도구라고 할 수 있다. 대표적으로는,
- JUnit 👉 Java (버전 5 출시) : 자바 프로그래밍 언어용 유닛 테스트 프레임워크
- DBUnit 👉 DB
- CppUnit 👉 C++
- NUnit 👉 .net (닷넷)
이번 포스팅에서는 JUnit4로 테스트 코드를 작성해볼 것이다.
기본 코드 작성
[참고] 필자의 build.gradle 파일 코드이다.
plugins {
id 'org.springframework.boot' version '2.5.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.moonz'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.testng:testng:7.1.0'
implementation 'junit:junit:4.13.1'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
간단한 테스트 코드를 위해
서버 구동에 필요한 main 클래스와 GET호출에 응답할 Controller 클래스 를 생성했다.
(main 클래스는 자동으로 생성되어있을 것이다.)
[main 클래스 코드]
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BookSpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(BookSpringbootApplication.class, args);
}
}
SpringApplication.run() ❔
내장 WAS(Web Application Server)를 실행시키는 역할을 한다.
내장 WAS ❔
Spring Boot에서는 기본적으로 내장 WAS로 톰캣을 사용한다. 그렇기 때문에 별도로 외부에 서버를 설치할 필요 없고, 스프링 부트로 만들어진 Jar파일(실행가능한 Java 패키징 파일)로 실행할 수 있다. 매우 편리하다!
내장 WAS를 권장하는 이유 ❔
Spring Boot에서 기본으로 제공하는 내장 WAS를 사용하길 권장하는 이유는 언제 어디서나 같은 환경에서 스프링부트를 배포할 수 있기 때문이다.
만약 새로운 서버가 추가되면 모든 서버가 같은 WAS 환경을 구축해야하고,
만약 서버의 버전을 변경해야할 경우 모든 서버의 버전을 바꿔줘야한다.
이는 시간이 오래걸리고, 무엇보다 실수할 가능성이 있다.
[controller 클래스 코드]
controller 패키지 내에 helloController 클래스를 생성한다. (controller 패키지 생성!)
아래 메서드는 문자열 "hello"를 반환한다.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
[HelloControllerTest 클래스 코드]
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {
@Autowired //스프링이 관리하는 빈(Bean) 주입 애너테이션
private MockMvc mvc; // 웹 API 테스트 시 사용하는 목 객체
@Test
public void hello가_리턴된다() throws Exception{
// given
String hello = "hello";
mvc.perform(get("/hello")) // 체이닝 지원. 여러 검증 기능을 이어서 선언 가능
.andExpect(status().isOk()) // 응답 상태 검증
.andExpect(content().string(hello)); //응답 본문의 내용 검증
}
}
❓ MockMvc 클래스
서블릿 컨테이너의 구동 없이, 시뮬레이션된 MVC 환경에 모의 HTTP 서블릿 요청을 전송하는 기능을 제공하는 유틸리티 클래스
👉 해당 클래스를 이용해 가짜 객체를 만들어서 애플리케이션 서버에 배포하지 않고도 스프링 MVC 동작을 재현할 수 있다.
❓ MockMvc 클래스의 메서드
MovkMvc 클래스에서 몇가지 메서드를 제공하는데, 체이닝을 지원하기 때문에 특정 메서드에서 반환하는 리턴 값에 대해 '.'을 통해 연달아 메서드를 적용할 수 있다.
1. perform()
요청을 전송하는 역할을 하여 리턴값으로 ResultActions 객체를 받는다.
ResultActions 객체는 리턴 값을 검증하고 확인할 수 있는 andExcpect() 메소드를 제공한다.
2. andExpect()
응답을 검증하는 역할을 한다.
위 코드에 있는 status().isOk(), content().string() 외에도 다양한 메서드를 인자로 받아 검증할 수 있다.
3. get()
HTTP 메소드를 결정하는 역할을 한다.( 이외에도post(), put(), delete() 가능)
인자로는 경로를 담는다.
[참고] 오류 발생 (2개)
❓ 첫번째 에러
Execution failed for task ':test'.
> No tests found for given includes : [문제 클래스 위치]
👉 이는 오류가 발생한 클래스에 테스트 클래스가 없다고 인식하여 발생하는 문제이다.
‼️ 해결
본인은 settings에서
Build, Execution, Deployment - Gradle - Run tests using 을 Gradle 👉 IntelliJ IDEA로 바꿔줌으로써 해결하였다.
👉 명령 실행자의 기본값이 Gradle이기 때문에 프로젝트를 build하거나 run할 때 Gradle로 실행됨으로써 발생한 문제이다.
필자의 생각으로는 필자의 Gradle과 JUnit4의 버전이 맞지 않아 발생한 문제인 것 같다.
(그리고 Gradle로 서버를 올리는 것이 더 오래 걸린다는 이야기도 있다.)
❓ 두번째 에러
No runnable methods
👉 실행할 메서드가 없다는 오류이다.
‼️ 해결
import org.testng.annotations.Test; 가 아닌
import org.junit.Test; 로 변경
👉 import가 알맞게 되었는지 확인해야한다!
테스트 코드 통과!
아래와 같이 Tests passed: 1 이 뜨면 성공이다.
이처럼 테스트 코드를 작성하고 자동 검증하는 것을 개발을 진행하면서 반복적으로 해야한다.
브라우저로 수동 검증하는 것 또한 한번씩 해줘도 좋지만, 절대 테스트코드를 먼저 작성 및 검증 후 가끔씩 체크하는 정도로만 해줘야한다.
롬복 작동 테스트
이번에는 모든 응답을 처리할 Dto 클래스를 dto 패키지 내에 생성해준다. (dto 패키지 생성!) - 롬복 이용!
[HelloResponseDto 코드]
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
// 모든 응답 Dto는 이 Dto 패키지에서!
public class HelloResponseDto {
private final String name;
private final int amount;
}
테스트 코드를 작성하자.
[HelloResponseDtoTest 테스트 코드]
import org.junit.Test;
public class HelloResponseDtoTest {
@Test
public void 롬복_기능_테스트() {
//given
String name = "test";
int amount = 1000;
//when
HelloResponseDto dto = new HelloResponseDto(name, amount);
//then
assertThat(dto.getName().isEqualTo(name));
assertThat(dto.getAmount().isEqualTo(amount));
}
}
테스트코드 통과 !
지금까지 롬복을 이용해서 ResponseDto 클래스를 생성했다.
그리고 ResponseDto에 값을 줬을 때 롬복이 잘 작동하는지 검증하였다.
이제 HelloController 클래스에 ResponseDto를 사용하는 API 를 추가해보자.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
// .. 생략
@GetMapping("/hello/dto")
public HelloResponseDto helloDto(@RequestParam String name, @RequestParam int amount) {
return new HelloResponseDto(name, amount); // api 호출하는 쪽에서 넘긴 인자로 설정
}
}
롬복 작동 테스트 2
작성하면서 라이브러리를 import하는데 신경써줘야 한다. 다른 라이브러리를 참고있을지도 모른다!!
이번에는 테스트 코드 작성!!!!!!!
[HelloControllerTest 코드 - 추가]
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.is; //추가
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; // content ,jsonPath, status 포함
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {
@Autowired //스프링이 관리하는 빈(Bean) 주입
private MockMvc mvc; //스프링 MVC 테스트의 시작점. 웹 API(GET, POST) 테스트 시 사용
@Test
public void hello가_리턴된다() throws Exception{
String hello = "hello";
mvc.perform(get("/hello")) // 체이닝 지원. 여러 검증 기능을 이어서 선언 가능
.andExpect(status().isOk())
.andExpect(content().string(hello)); //응답 본문의 내용 검증
}
// 추가
@Test
public void helloDto가_리턴된다() throws Exception {
String name="hello";
int amount = 1000;
mvc.perform(
get("/hello/dto")
.param("name", name)
.param("amount", String.valueOf(amount)))
.andExpect(status().isOk())
.andExpect(jsonPath("name", is(name)));
}
}
❓ assertThat()
- assertj라는 테스트 검증 라이브러리의 검증 메서드
- 검증하고 싶은 대상을 메소드 인자로 받는다.
- 메소드 체이닝이 지원되서 메소드를 이어서 사용 가능하다.
❔ Junit의 assertThat과 assertj의 assertThat : 백기선님의 영상
❓ RunWith()
- JUnit에 내장된 실행자 외에 다른 실행자 실행하도록하는 애노테이션
- 위 코드에서는 SpringRunner라는 스프링 실행자를 사용함
- 스프링부트 테스트와 JUnit 사이 연결자 역할
- JUnit5부터는 runWith()를 사용하지 않고 extendWith()를 사용하는데, 이제는 SpringBoot에서 이를 적극적으로 활용하여 @SpringBootTest만 붙여주면 된다. (해당 애노테이션 안에 extendWith()가 포함되어있는 것을 확인할 수 있다.)
❓ WebMvcTest()
- 여러 test annotation 중 Web(Spring MVC)에 집중할 수 있도록 해주는 애노테이션
- 단 @Service, @Component, @Repository 사용 불가
- @Controller, @ControllerAdvice 사용 가능
❓ param(키, 값)
- MockMvc 클래스의 메서드로, API 요청 중 전달할 파라미터가 있을 때 사용한다.
- 메서드의 인자값에 값을 담아서 전달한다.
- But, 값은 String만 가능 (String.valueOf() 로 변환 후 가능)
❓ jsonPath()
- JSON 응답값을 필드마다 검증할 수 있는 메서드
- $를 기준으로 필드명 명시 ex) $.name, $.amount
롬복 테스트 성공
기초적인 코드와 롬복 기능에 대한 테스트를 해보았다.
언젠가 TDD(테스트 주도 개발) 방식의 개발을 하고 있는 내 모습을 상상하며
프로젝트 개발 시, 테스트 코드를 작성하고 검증하는 과정을 습관화하고자 한다.
'백엔드 개발하며 작성한 > Spring' 카테고리의 다른 글
JAP Query로 특정 칼럼의 count 쿼리문 실행하기 (0) | 2021.11.12 |
---|---|
[Spring Boot 프로젝트] AWS EC2로 Spring Boot 배포 (0) | 2021.10.16 |
[Spring Boot 프로젝트] 1. 프로젝트 생성과 Boot Strap 템플릿 적용(Thymeleaf) (0) | 2021.10.08 |
ORM(Object Relational Mapping)과 JPA(Java Persistence API) (0) | 2021.10.07 |
[Spring Boot] 프로젝트 세팅부터 REST API까지 (0) | 2021.09.26 |