From 07cd696f1d92e5a62106f3787062824d3db5693e Mon Sep 17 00:00:00 2001 From: banjjoknim Date: Tue, 17 Nov 2020 19:50:29 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8A=A4=ED=94=84=EB=A7=81=20=EB=B6=80?= =?UTF-8?q?=ED=8A=B8=EC=99=80=20AWS=EB=A1=9C=20=ED=98=BC=EC=9E=90=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=98=EB=8A=94=20=EC=9B=B9=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20Chapter2=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 + WebServiceBySpringBootAndAWS/build.gradle | 1 + .../src/ChapterDescription/Chapter2.md | 246 ++++++++++++++++++ .../book/springboot/Application.java | 11 + .../book/springboot/web/HelloController.java | 20 ++ .../springboot/web/dto/HelloResponseDto.java | 12 + .../springboot/web/HelloControllerTest.java | 41 +++ .../web/dto/HelloResponseDtoTest.java | 22 ++ 8 files changed, 363 insertions(+) create mode 100644 WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter2.md create mode 100644 WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/Application.java create mode 100644 WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/HelloController.java create mode 100644 WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/dto/HelloResponseDto.java create mode 100644 WebServiceBySpringBootAndAWS/src/test/java/com/banjjoknim/book/springboot/web/HelloControllerTest.java create mode 100644 WebServiceBySpringBootAndAWS/src/test/java/com/banjjoknim/book/springboot/web/dto/HelloResponseDtoTest.java diff --git a/.gitignore b/.gitignore index 1e79e09..0379f71 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,13 @@ TIL(Today-I-Learned).iml .idea/ WebServiceBySpringBootAndAWS/settings.gradle WebServiceBySpringBootAndAWS/gradle/wrapper/gradle-wrapper.properties +WebServiceBySpringBootAndAWS/build/reports/tests/test/index.html +WebServiceBySpringBootAndAWS/build/reports/tests/test/classes/com.banjjoknim.book.springboot.web.HelloControllerTest.html +WebServiceBySpringBootAndAWS/build/reports/tests/test/css/base-style.css +WebServiceBySpringBootAndAWS/build/reports/tests/test/css/style.css +WebServiceBySpringBootAndAWS/build/reports/tests/test/js/report.js +WebServiceBySpringBootAndAWS/build/reports/tests/test/packages/com.banjjoknim.book.springboot.web.html +WebServiceBySpringBootAndAWS/build/test-results/test/TEST-com.banjjoknim.book.springboot.web.HelloControllerTest.xml +WebServiceBySpringBootAndAWS/build/test-results/test/binary/output.bin +WebServiceBySpringBootAndAWS/build/test-results/test/binary/output.bin.idx +WebServiceBySpringBootAndAWS/build/test-results/test/binary/results.bin diff --git a/WebServiceBySpringBootAndAWS/build.gradle b/WebServiceBySpringBootAndAWS/build.gradle index 9e65eb0..8c69eac 100644 --- a/WebServiceBySpringBootAndAWS/build.gradle +++ b/WebServiceBySpringBootAndAWS/build.gradle @@ -26,5 +26,6 @@ repositories { dependencies { compile('org.springframework.boot:spring-boot-starter-web') + compile('org.projectlombok:lombok') testCompile('org.springframework.boot:spring-boot-starter-test') } diff --git a/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter2.md b/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter2.md new file mode 100644 index 0000000..76c8e6d --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter2.md @@ -0,0 +1,246 @@ +# Chapter2. 스프링 부트에서 테스트 코드를 작성하자 + +--- + +## 2.1 테스트 코드 소개 + +`TDD`와 단위 테스트는 다른 이야기이다. `TDD`는 **테스트가 주도하는 개발**을 이야기하는데, **테스트 코드를 먼저 작성**하는 것부터 시작합니다. + +>**레드 그린 사이클** +>- 항상 실패하는 테스트를 먼저 작성하고(`Red`) +>- 테스트가 통과하는 프로덕션 코드를 작성하고(`Green`) +>- 테스트가 통과하면 프로덕션 코드를 리팩토링 합니다(`Refactor`). + +반면 단위 테스트는 `TDD`의 첫 번째 단계인 **기능 단위의 테스트 코드를 작성**하는 것을 이야기한다. `TDD`와 달리 테스트 코드를 꼭 먼저 작성해야 하는 것도 아니고, 리팩토링도 포함되지 않는다. 순수하게 테스크 코드만 작성하는 것을 말한다. + +>**단위 테스트 코드를 작성함으로써 얻는 이점** +>- 단위 테스트는 개발단계 초기에 문제를 발견하게 도와줍니다. +>- 단위 테스트는 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 등에서 기존 기능이 올바르게 작동하는지 확인할 수 있습니다(예, 회귀테스트). +>- 단위 테스트는 기능에 대한 불확실성을 감소시킬 수 있습니다. +>- 단위 테스트는 시스템에 대한 실제 문서를 제공합니다. 즉, 단위 테스트 자체가 문서로 사용할 수 있습니다. + +--- + +## 2.2 Hello Controller 테스트 코드 작성하기 + +- 일반적으로 패키지 명은 **웹 사이트 주소의 역순**으로 합니다. 예를 들어 `admin.jojoldu.com`이라는 사이트라면 패키지명은 `com.jojoldu.admin`으로 하면 됩니다. +- `@SpringBootApplication` 어노테이션으로 인해 스프링 부트의 자동 설정, 스프링 `Bean` 읽기와 생성이 모두 자동으로 설정됩니다. +- 특히, `@SpringBootApplication`이 있는 위치부터 설정을 읽어가기 때문에 이 어노테이션이 선언된 클래스는 항상 **프로젝트의 최상단에 위치**해야만 합니다. +- 내장 WAS(`Web Application Server`)란 별도로 외부에 WAS를 두지 않고 애플리케이션을 실행할 때 내부에서 WAS를 실행하는 것을 이야기합니다. `SpringApplication.run`으로 인해 내장 WAS를 실행합니다. 이렇게 되면 항상 서버에 **톰캣을 설치할 필요가 없게 되고,** 스프링 부트로 만들어진 `Jar` 파일(실행 가능한 Java 패키징 파일)로 실행하면 됩니다. +- 스프링 부트에서는 **내장 WAS를 사용하는 것을 권장**한다. **언제 어디서나 같은 환경에서 스프링 부트를 배포**할 수 있기 때문이다. +> +- **컨트롤러와 관련된 클래스들은 모두 web 패키지에 추가** + +```java +package com.banjjoknim.book.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"; + } +} +``` + +**1. @RestController** +- 컨트롤러를 `JSON`을 반환하는 컨트롤러로 만들어 줍니다. +- 예전에는 `@ResponseBody`를 각 메소드마다 선언했던 것을 한번에 사용할 수 있게 해준다고 생각하면 됩니다. + +**2. @GetMapping** +- `HTTP Method`인 `Get`의 요청을 받을 수 있는 API를 만들어 줍니다. +- 예전에는 `@RequestMapping(method = RequestMethod.GET)`으로 사용되었습니다. + +```java +package com.banjjoknim.book.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. + } +} +``` +**1. @RunWith(SpringRunner.class)** +- 테스트를 진행할 때 `JUnit`에 내장된 실행자 외에 다른 실행자를 실행시킵니다. +- 여기서는 `SpringRunner`라는 스프링 실행자를 사용합니다. +- 즉, 스프링 부트 테스트와 `JUnit` 사이에 연결자 역할을 합니다. + +**2. @WebMvcTest** +- 여러 스프링 테스트 어노테이션 중, `Web(Spring MVC)`에 집중할 수 있는 어노테이션입니다. +- 선언할 경우 `@Controller`, `@ControllerAdvice` 등을 사용할 수 있습니다. +- 단, `@Service`, `@Component`, `@Repository` 등은 사용할 수 없습니다. +- 여기서는 컨트롤러만 사용하기 때문에 선언합니다. + +**3. @Autowired** +- 스프링이 관리하는 빈(`Bean`)을 주입 받습니다. + +**4. private MockMvc mvc** +- 웹 API를 테스트할 때 사용합니다. +- 스프링 `MVC` 테스트의 시작점입니다. +- 이 클래스를 통해 `HTTP GET`, `POST` 등에 대한 API 테스트를 할 수 있습니다. + +**5. mvc.perform(get("/hello"))** +- `MockMvc`를 통해 `/hello` 주소로 `HTTP GET` 요청을 합니다. +- 체이닝이 지원되어 아래와 같이 여러 검증 기능을 이어서 선언할 수 있습니다. + +**6. .andExpect(status().isOk())** +- `mvc.perform`의 결과를 검증합니다. +- `HTTP Header`의 `Status`를 검증합니다. +- 우리가 흔히 알고 있는 `200`, `404`, `500` 등의 상태를 검증합니다. +- 여기선 `OK` 즉, `200`인지 아닌지를 검증합니다. + +**7. .andExpect(content().string(hello))** +- `mvc.perform`의 결과를 검증합니다. +- 응답 본문의 내용을 검증합니다. +- `Controller`에서 `"hello"`를 리턴하기 때문에 이 값이 맞는지 검증합니다. + +--- + +## 2.3 롬복 소개 및 설치하기 + +**자바 개발자들의 필수 라이브러리 롬복** +- 롬복은 자바 개발할 때 자주 사용하는 코드 `Getter`, `Setter`, 기본생성자, `toString` 등을 어노테이션으로 자동 생성해 줍니다. + + +- `build.gradle`에 다음의 코드를 추가하여 의존성(라이브러리)을 추가합니다. +```java +compile('org.projectlombok:lombok') +``` + +- `plugins`에서 `lombok` 플러그인을 검색하여 설치합니다. + +- 롬복은 프로젝트마다 설정해야 합니다. 플러그인 설치는 한 번만 하면 되지만, `build.gradle`에 라이브러리를 추가하는 것과 `Enable annotation processing`를 체크하는 것은 프로젝트마다 진행해야 합니다. + +--- + +## 2.4 Hello Controller 코드를 롬복으로 전환하기 + +- **모든 응답 Dto는 Dto 패키지에 추가** + +```java +package com.banjjoknim.book.springboot.web.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter // 1. +@RequiredArgsConstructor // 2. +public class HelloResponseDto { + + private final String name; + private final int amount; +} +``` + +**1. @Getter** +- 선언된 모든 필드의 `get` 메소드를 생성해줍니다. + +**2. @RequiredArgsConstructor** +- 선언된 모든 `final` 필드가 포함된 생성자를 생성해 줍니다. +- `final`이 없는 필드는 생성자에 포함되지 않습니다. + +```java +package com.banjjoknim.book.springboot.web.dto; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.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); + } +} +``` + +**1. assertThat** +- `assertj`라는 테스트 검증 라이브러리의 검증 메소드입니다. +- 검증하고 싶은 대상을 메소드 인자로 받습니다. +- 메소드 체이닝이 지원되어 `isEqualTo`와 같이 메소드를 이어서 사용할 수 있습니다. + +**2. isEqualTo** +- `assertj`의 동등 비교 메소드입니다. +- `assertThat`에 있는 값과 `isEqualTo`의 값을 비교해서 같을 때만 성공입니다. + +**Junit과 비교하여 assertj의 장점은 다음과 같습니다.** +- `CoreMatchers`와 달리 추가적으로 라이브러리가 필요하지 않습니다. + - `JUnit`의 `assertThat`을 쓰게 되면 `is()`와 같이 `CoreMatchers` 라이브러리가 필요합니다. +- 자동완성이 좀 더 확실하게 지원됩니다. + - IDE에서는 `CoreMatchers`와 같은 `Matcher` 라이브러리의 자동완성 지원이 약합니다. + +```java +@GetMapping("/hello/dto") + public HelloResponseDto helloDto(@RequestParam("name") String name, @RequestParam("amount") int amount) { // 1. + return new HelloResponseDto(name, amount); + } +``` + +**1. @RequestParam** +- 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션입니다. +- 여기서는 외부에서 `name (@RequestParam("name"))`이란 이름으로 넘긴 파라미터를 메소드 파라미터 `name (String name)`에 저장하게 됩니다. + +`name`과 `amount`는 API를 호출하는 곳에서 넘겨준 값들입니다. 추가된 API를 테스트하는 코드를 `HelloControllerTest`에 추가합니다. + +```java +@Test + public void helloDto가_리턴된다() throws Exception { + String name = "hello"; + int amount = 1000; + + mvc.perform(get("/hello/dto") + .param("name", name) // 1. + .param("amount", String.valueOf(amount))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name", is(name))) // 2. + .andExpect(jsonPath("$.amount", is(amount))); + } +``` + +**1. param** +- API 테스트할 때 사용될 요청 파라미터를 설정합니다. +- 단, 값은 `String`만 허용됩니다. +- 그래서 숫자/날짜 등의 데이터도 등록할 때는 문자열로 변경해야만 가능합니다. + +**2. jsonPath** +- `JSON` 응답값을 필드별로 검증할 수 있는 메소드입니다. +- `$`를 기준으로 필드명을 명시합니다. +- 여기서는 `name`과 `amount`를 검증하니 `$.name`, `$.amount`로 검증합니다. + +--- \ No newline at end of file diff --git a/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/Application.java b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/Application.java new file mode 100644 index 0000000..f8a293d --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/Application.java @@ -0,0 +1,11 @@ +package com.banjjoknim.book.springboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/HelloController.java b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/HelloController.java new file mode 100644 index 0000000..433ad3c --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/HelloController.java @@ -0,0 +1,20 @@ +package com.banjjoknim.book.springboot.web; + +import com.banjjoknim.book.springboot.web.dto.HelloResponseDto; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @GetMapping("/hello") + public String hello() { + return "hello"; + } + + @GetMapping("/hello/dto") + public HelloResponseDto helloDto(@RequestParam("name") String name, @RequestParam("amount") int amount) { + return new HelloResponseDto(name, amount); + } +} diff --git a/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/dto/HelloResponseDto.java b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/dto/HelloResponseDto.java new file mode 100644 index 0000000..fb774b5 --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/main/java/com/banjjoknim/book/springboot/web/dto/HelloResponseDto.java @@ -0,0 +1,12 @@ +package com.banjjoknim.book.springboot.web.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class HelloResponseDto { + + private final String name; + private final int amount; +} diff --git a/WebServiceBySpringBootAndAWS/src/test/java/com/banjjoknim/book/springboot/web/HelloControllerTest.java b/WebServiceBySpringBootAndAWS/src/test/java/com/banjjoknim/book/springboot/web/HelloControllerTest.java new file mode 100644 index 0000000..efbc08b --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/test/java/com/banjjoknim/book/springboot/web/HelloControllerTest.java @@ -0,0 +1,41 @@ +package com.banjjoknim.book.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.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@RunWith(SpringRunner.class) +@WebMvcTest(controllers = HelloController.class) +public class HelloControllerTest { + @Autowired + private MockMvc mvc; + + @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))) + .andExpect(jsonPath("$.amount", is(amount))); + } +} \ No newline at end of file diff --git a/WebServiceBySpringBootAndAWS/src/test/java/com/banjjoknim/book/springboot/web/dto/HelloResponseDtoTest.java b/WebServiceBySpringBootAndAWS/src/test/java/com/banjjoknim/book/springboot/web/dto/HelloResponseDtoTest.java new file mode 100644 index 0000000..71da4ae --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/test/java/com/banjjoknim/book/springboot/web/dto/HelloResponseDtoTest.java @@ -0,0 +1,22 @@ +package com.banjjoknim.book.springboot.web.dto; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.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); + assertThat(dto.getAmount()).isEqualTo(amount); + } +} \ No newline at end of file