견고한 서비스를 만들기 위해선 테스트 코드 / TDD가 매우 중요하다
최근 대부분의 회사에서 테스트 코드에 관해 요구하고 있으며 단위 테스트에 대한 경험을 필수조건으로 둔다
이번 장에선 테스트 코드 작성의 기본을 알아볼 것이다
TDD
단위 테스트
이번 장에선 단위 테스트 코드를 학습하며, 추후 TDD를 학습하길 추천한다
단위 테스트 코드 작성의 이점은 다음과 같다(위키피디아)
조금 더 실용적인 이점은 다음과 같다
1) 빠른 피드백
단위 테스트 이전의 개발 프로세스는 다음과 같았다
System.out.println()
으로 눈으로 검증여기서, 2~5의 과정은 코드가 수정될 때 마다 반복하는 작업이며, 특히 톰캣을 내렸다가 다시 실행하는 것은 많은 시간이 소요된다
테스트 코드가 없다 보니 눈과 손으로 직접 수정된 기능을 확인할 수 밖에 없어서 생긴 문제이다
2) 자동검증
System.out.println()
을 통해 눈으로 검증하다 보니, 놓치는 부분이 있을 수 있다
테스트 코드를 작성한다면 자동검증이 가능하다
3) 개발자가 만든 기능을 안전하게 보호
시스템에 B라는 기능을 추가 할 때, 기존에 잘 되던 A라는 기능이 안되는 경우가 발생한다
새로운 기능을 추가할 때마다 서비스의 모든 기능을 테스트 하기엔 자원과 시간이 너무 많이 든다
테스트 코드를 구현해 놓는다면, 새로운 기능이 추가 될 때마다 테스트 코드를 수행하기만 하면 되므로 테스트 코드는 기존 기능이 잘 작동하는 것을 보장해 준다
가장 대중적인 테스트 프레임워크는 xUnit이다
개발환경(x)에 따른 Unit 테스트를 도와주는 도구
이 책에서는 JUnit4를 사용한다
1장에서 만든 프로젝트에서 패키지를 생성한다
src → main → java → 마우스 우클릭 → New → Package
패키지명은 일반적으로 웹사이트 주소와 반대로 명명한다
1장에서 사용한 Group ID인 com.vencott.dev
에 현재 프로젝트명인 springboot
를 덧붙인 com.vencott.dev.springboot
로 생성한다
그 다음, Java 클래스를 생성한다
패키지 → 마우스 우클릭 → New → Java class
클래스의 이름은 Application으로 하고 다음과 같이 코드를 작성한다
src/main/java/com/vencott/dev/springboot/Application.java
package com.vencott.dev.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication // 1
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args); // 2
}
}
Application 클래스는 프로젝트의 메인 클래스이다
@SpringBootApplication
SpringApplication.run()
먼저, 최상위 패키지 안에 web
패키지를 생성한다
HelloController라는 이름으로 컨트롤러를 하나 생성한다
src/main/java/com/vencott/dev/springboot/web/HelloController.java
package com.vencott.dev.springboot.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController // 1
public class HelloController {
@GetMapping("/hello") // 2
public String hello() {
return "hello";
}
}
@RestController
@RequsetBody
를 각 메소드마다 선언했던 것을 한번에 해준다고 생각한다@GetMapping
@RequsetMapping(method = RequestMethod.GET)
과 같은 기능이제, 테스트 코드로 검증할 차례이다
src/test/java
디렉토리에 앞에서 작성한 패키지와 동일한 패키지를 생성한다src/test/java/com/vencott/dev/springboot/web/HelloControllerTest.java
package com.vencott.dev.springboot.web;
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;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class) // 1
@WebMvcTest(controllers = HelloController.class) // 2
public class HelloControllerTest {
@Autowired // 3
private MockMvc mvc; // 4
@Test
public void hello가_리턴된다() throws Exception {
String hello = "hello";
mvc.perform(get("/hello")) // 5
.andExpect(status().isOk()) // 6
.andExpect(content().string(hello)); // 7
}
}
@RunWith(SpringRunner.class)
@WebMvcTest
@AutoWired
private MockMvc mvc
mvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(hello))
메소드 왼쪽의 화살표를 클릭한다
다음과 같이 테스트가 통과한 것을 확인할 수 있다
우리가 검증하고자 했던.andExpect(status().isOk())
와 .andExpect(content().string(hello))
가 모두 테스트를 통과한 것을 의미한다
테스트 코드가 아닌 수동으로 앱을 실행시켜 결과를 확인해본다
Application.java
파일로 이동해 main 메소드의 왼쪽 화살표 버튼을 클릭한 후, Run 'Application.main()' 버튼을 클릭한다
실행 후 스프링 부트 로그에 톰캣 서버가 8080 포트로 실행되었다는 것이 출력된다
웹 브라우저에서 localhost:8080/hello로 접속해 테스트 코드와 결과가 같은 것을 확인한다
절대 수동으로 검증하고 테스트 코드를 작성하지 않는다
테스트 코드로 먼저 검증 후, 다시 한번 확인할 때 프로젝트를 실행해 확인하는 순서를 잊지 말아야 한다
테스트 코드를 꼭 작성해야 견고한 소프트웨어를 만드는 역량이 성장할 수 있다
롬복은 자바 개발자들의 필수 라이브러리로 Getter, Setter, 기본생성자, toString 등을 어노테이션으로 자동 생성해 준다
인텔리제이는 플러그인 덕분에 쉽게 롬복을 사용할 수 있다
build.gradle에 의존성을 추가한 후 Refresh한다
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
Plugin에서 Lombok을 설치한다
Settings > Build > Compiler > Annotation Processors에서 Enable annotation processing 옵션을 체크한다
기존 코드를 롬복으로 변경하는 일은 규모가 큰 프로젝트면 쉽지 않은 일이다
어떤 기능이 제대로 작동될지 안 될지 예측하기 어렵기 때문이다
하지만 테스트 코드를 잘 작성했다면 롬복으로 전환하고 테스트 코드만 돌려보면 된다
web 패키지에 dto 패키지를 추가한다
앞으로 모든 응답 Dto는 이 Dto 패키지에 추가한다
src/main/java/com/vencott/dev/springboot/web/dto/HelloResponseDto.java
package com.vencott.dev.springboot.web.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter // 1
@RequiredArgsConstructor // 2
public class HelloResponseDto {
private final String name;
private final int amount;
}
@Getter
@RequiredArgsConstructor
Dto에 적용된 롬복이 잘 작동하는지 테스트 코드를 작성한다
src/test/java/com/vencott/dev/springboot/web/dto/HelloResponseDtoTest.java
package com.vencott.dev.springboot.web.dto;
import org.junit.Test;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
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); // 1, 2
assertThat(dto.getAmount()).isEqualTo(amount);
}
}
assertThat
assertThat
이 아닌 assertj의 assertThat
로 다음과 같은 장점이 있다isEqualTo
assetThat
에 있는 값과 isEqualTo의 값을 비교해 같을 때만 성공한다테스트를 실행해 보면 정상적으로 수행됨을 확인할 수 있다
롬복의 @Getter
로 get 메소드가, @RequiredArgsConstructor
로 생성자가 자동으로 생성되는 것이 증명된 것이다
먼저 HelloController에 새로 만든 ResponseDto를 사용하도록 코드를 추가한다
src/main/java/com/vencott/dev/springboot/web/HelloController.java
@GetMapping("/hello/dto")
public HelloResponseDto helloDto(@RequestParam("name") String name, @RequestParam("amount") int amount) { // 1
return new HelloResponseDto(name, amount);
}
@RequestParam
추가된 API를 테스트하는 코드를 HelloControllerTest에 추가한다
src/test/java/com/vencott/dev/springboot/web/HelloControllerTest.java
@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))) // 1
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", is(name))) // 2
.andExpect(jsonPath("$.amount", is(amount)));
}
param
jsonPath
테스트 결과, JSON이 리턴되는 API 역시 정상적으로 통과됨을 확인한다
출처: 이동욱 저, 『스프링 부트와 AWS로 혼자 구현하는 웹 서비스』, 프리렉(2019)