Java & Spring/스프링 부트와 AWS로 혼자 구현하는 웹 서비스

2장) 스프링 부트에서 테스트 코드를 작성하자

Zin0_0 2020. 6. 27. 01:24
반응형

테스트 코드 소개

  • TDD vs 단위 테스트
    • TDD - 테스트가 주도하는 개발 (테스트 코드를 먼저 작성하는 것부터 시작)
    • 레드 그린 사이클
      • 항상 실패하는 테스트를 먼저 작성하고(Red)
      • 테스트가 통과하는 프로덕션 코드를 작성하고(Green)
      • 테스트가 통과하면 프로덕션 코드를 리팩토링함(Refactor)
    • 단위 테스트 - TDD의 첫 번째 단계인 기능 단위의 테스트 코드를 작성하는 것
      • 따라서, 테스트 코드를 먼저 작성하지 않아도, 리팩토링을 포함하지 않아도 됨
      • 개발 단계 초기에 문제를 발견하게 도와준다.
      • 리팩토링을 하거나 라이브러리 업그레이드 등에서 기존 기능이 올바르게 작동하는지 확인할 수 있음
      • 기능에 대한 불확실성을 감소
      • 시스템에 대한 실제 문서를 제공
    • 단위 테스트 장점
      • 빠른 피드백
      • 자동검증이 가능
      • 개발자가 만든 기능을 안전하게 보호 ~> 새로운 기능이 추가될 때, 기존 기능이 잘 작동되는 것을 보장
    • xUnit - 테스트 프레임워크
      • x(개발 환경)에 따라 Unit 테스트를 도와주는 도구
      • Java - JUnit, DB - DBUnit ...
  • Hello Controller 테스트 코드 작성하기

    • 일반적으로 패키지 명은 웹 사이트 주소의 역순으로 만듬

      • ex)admin.zin0.com 사이트 -> com.zin0.admin 패키지명
    • 앞으로 만들 프로젝트의 메인 클래스가 될 Application 클래스를 만든다.

      package com.zin0.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);
          }
      }
    • @SpringBootApplication - 스프링 부트의 자동 설정, 스프링 Bean 읽기와 생성을 모두 자동으로 설정

    • @SpringBootApplication이 있는 위치부터 설정을 읽어 나가기 때문에, 이 클래스는 항상 프로젝트의 최상단에 위치해야만 한다.

    • main 메소드의 SpringApplication.run으로 인해 내장 WAS를 실행

    • 내장 WAS - 언제 어디서나 같은 환경에서 스프링 부트를 배포할 수 있음

  • 컨트롤러와 관련된 것을 담을 패키지(여기서는 web)을 만든다.

  • HelloController 클래스를 만든다.

    package com.zin0.book.springboot.web;
    
    import com.zin0.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";
        }
    }
  • @RestController

    • 컨트롤러를 JSON을 반환하는 컨트롤러로 만들어 준다.
    • @ResponseBody를 각 메소드마다 선언했던 것을 한번에 사용할 수 있게 해준다고 생각하면 편함
  • @GetMapping

    • Get 요청을 받을 수 있는 API를 만든다.
  • 테스트 코드 검증을 위해, test에 HelloControllerTest 클래스를 생성한다.

    • 검증할 것과 패키지 이름이 같아야함(실습하다가 에러를 반환하길래 패키지명에 오타가 있어서 바꿔줬더니 됐음)

    • 보통 클래스 이름은 검증할 클래스 이름에 Test를 붙임

      package com.zin0.book.springboot;
      
      import com.zin0.book.springboot.web.HelloController;
      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;
      import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
      import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
      
      @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));
          }
      }
  • @RunWith(SpringRunner.class)

    • 테스트 진행 시, JUnit에 내장된 실행자 외에 다른 실행자를 실행시킴
    • 여기서는 SpringRunner라는 스프링 실행자를 사용
    • 즉, 스프링 부트 테스트와 JUnit 사이에 연결자 역할
  • @WebMvcTest

    • 여러 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션
    • 선언할 경우 @Controller, @ControllerAdvice 등을 사용 가능
    • 단, @Service, @Component, @Repository 등 사용 불가
    • 위의 클래스는 Controller 클래스기 때문에 선언함
  • @Autowired

    • 스프링이 관리하는 빈(Bean)을 주입 받음
  • @private MockMvc mvc

    • 웹 API를 테스트할 때 사용 (Get, Post 등)
    • 스프링 MVC 테스트의 시작점
  • mvc.perform(get("hello"))

    • MockMvc를 통해 /hello 주소로 GET 요청을 보냄
    • 체이닝이 지원 ~> 따라서 여러 검증 기능을 이어서 선언 가능
  • .andExpect(status().isOk())

    • mvc.perform의 결과를 검증
    • HTTP Header의 Status를 검증 ( 200, 404, 500 등 상태)
    • 여기선 200인지 아닌지 검증
  • .andExpect(content().string(hello))

    • mvc.perform의 결과 검증 ~> 응답 본문의 내용을 검증
    • Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 검증

  • 롬복 소개 및 설치

    • 롬복 - 자바 개발자들의 필수 라이브러리
      • Getter, Setter, 기본 생성자, toString 등을 어노테이션으로 자동 생성해준다.
      • build.gradle의 의존성 부분에 compile('org.projectlombok:lombok')을 추가
      • [ Ctrl + Shift + A ]를 통해 Plugins를 선택해주고 Marketplace에서 롬복 플러그인을 인스톨해준다. (인스톨 후 재시작 필수) ~> 알림창 Enable 클릭 or "Setting -> Build -> Compiler -> Annotation Processors"에서 Enable 설정
  • Hello Controller 코드를 롬복으로 전환하기

    • 테스트 코드가 작성한 코드를 지켜주기 때문에, 쉽게 변경할 수 있다.

    • web 패키지에 모든 응답 Dto를 담을 dto 패키지를 추가

    • HelloResponseDto 클래스를 생성

      package com.zin0.book.springboot.web.dto;
      
      import lombok.Getter;
      import lombok.RequiredArgsConstructor;
      
      @Getter
      @RequiredArgsConstructor
      public class HelloResponseDto {
          private final String name;
          private final int amount;
      }
    • @Getter

      • 선언된 모든 필드의 get 메소드를 생성
    • @RequiredArgsConstructor

      • 선언된 모든 final 필드가 포함된 생성자를 생성
      • final이 없는 필드는 생성자에 포함 X
    • HelloResponseDtoTest 클래스 생성

      package com.zin0.book.springboot.dto;
      
      import com.zin0.book.springboot.web.dto.HelloResponseDto;
      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);
          }
      }
    • aseertThat

      • assertj라는 테스트 검증 라이브러리의 검증 메소드
      • 검증하고 싶은 대상을 메소드 인자로 받음
      • 메소드 체이닝 지원
    • Junit의 기본 assertThat이 아닌 assertj의 assertThat을 사용한 이유

      • Junit의 assertThat을 쓰게 되면, is()와 같이 CoreMatchers 라이브러리가 필요하다.
      • 하지만, Junit은 추가적 라이브러리가 필요 X
      • assertj는 Junit에서 자동으로 라이브러리 등록을 해줌
  • HelloController에 ResponseDto 사용하도록 추가

    package com.zin0.book.springboot.web;
    
    import com.zin0.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);
        }
    }
  • @RequestParam

    • 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션
  • HelloControllerTest에 테스트 코드 작성

    package com.zin0.book.springboot;
    
    import com.zin0.book.springboot.web.HelloController;
    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;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
    
    @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)));
        }
    }
  • param

    • API 테스트할 때 사용될 요청 파라미터를 설정
    • 값은 String만 허용 (숫자/날짜 등 데이터도 문자열로 변경해주어야함)
  • jsonPath

    • JSON 응답값을 필드별로 검증
    • $를 기준으로 필드명 명시 해줘야함

추가)

main의 Application 클래스를 직접 확인하는 과정에서, Error를 반환

(8080포트가 이미 점유되었다고 반환)

  1. cmd를 통해서 port를 죽이려고 시도했지만 실패했다.

  2. /f를 써서 강제로 지우려고 했지만, 어떤 프로세스의 자식 프로세스라서 종료할 수 없다고 나왔다.

  3. 그래서, 연결 포트를 바꿔주기로 결정했다.
    -> resources에 있는 applications.properties에
    server.port = 원하는 포트번호
    를 추가해서 연결 포트를 바꿔주었다.
    (applications.properties가 없어서 만들어줬음)

그리고 신기한 점은 테스트 클래스에서 메소드를 만들 때, 한글로 적어도 지원이 된다는 것이었다..!!!

반응형