개발 공부

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 2장 - 테스트 코드 본문

Spring/Spring Boot

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 2장 - 테스트 코드

hyecozy 2022. 8. 8. 18:52

1장 정리글

https://paradiseiswhereiam.tistory.com/131

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 1장 (책과 다른 부분 수정)

2019년도에 나온 책이라 그런지 업데이트 된 부분이 중간중간 있어 블로그에 정리해 보려고 한다. 현재까지 수정한 부분은 다음과 같으며 순서대로 작성하였다. 1. p33 build.gradle 맨 위에 위치할 코

paradiseiswhereiam.tistory.com

 

 

테스트 코드란?

 

✔️ TDD(테스트가 주도하는 개발)

: 항상 실패하는 테스트 먼저 작성 ➡️ 테스트가 통과하는 프로덕션 코드 작성 ➡️ 테스트 통과하면 프로덕션 코드 리팩토링

 

✔️단위 테스트

: TDD의 첫 번째 단계인 기능 단위의 테스트 코드를 작성하는 것

 

⚫ 왜 테스트 코드가 필요한가?

테스트 코드를 쓰기 전에는 코드 작성 후,

< 프로그램을 톰캣으로 실행 후, API 테스트 도구로 HTTP 요청, 요청 결과를 System.out.println()으로 눈으로 검증, 결과가 다르면 다시 톰캣 중지 후 코드 수정>

이 꺾쇠 안의 과정을 코드를 수정할 때마다 써야 했다.

수정된 기능을 이렇게 톰캣을 실행시켜 직접 확인하지 않으면 확인할 수 없었기 때문이다.

 

테스트 코드는 구현 도구에서 바로 실행 가능하므로

톰캣을 쓸 필요가 없으며 톰캣 실행에 드는 시간도 아낄 수 있다.

그리고 System.out.prinln()을 써서 사람이 직접 검증하지 않아도 되고,

개발자가 만든 기능안전하게 보호해 준다. (테스트 코드를 수행한 기능은 잘 작동하는 기능이라고 보장 가능.)

 

Hello Controller 테스트 코드 작성하기

패키지명은 따로 적지 않겠습니다!

 

1. Application.java

package com.jojoldu.book.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication //스프링 부트의 자동설정, 스프링 Bean읽기와 생성 모두 자동으로 설정
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

앞으로 만들 프로젝트의 메인 클래스가 될 클래스.

@SpringBootApplication이 있는 위치부터 설정을 읽어가기 때문에 이 클래스는 항상 프로젝트최상단에 위치해야 함

 

그리고 SpringApplication.run으로 인해 내장 WAS가 실행

➡️ 톰캣이 필요없어지게 됨, 스프링 부트로 만들어진 Jar 파일로 실행하면 됨.

⚫왜 내장 WAS를 사용하는가?

: 스프링 부트에서도 권장하고 있고, 언제 어디서나 같은 환경에서 스프링 부트를 배포할 수 있기 때문!

ex. 새로운 서버가 추가 될 때마다 n대의 서버에 설치 된 WAS의 버전을 일일히 올려야 됨

 

 

2. HelloController.java

간단하게 API 만들기

package com.jojoldu.book.springboot.web;

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";
    }
}

@RestController : 컨트롤러를 JSON으로 반환하는 컨트롤러로 만들어 줌

이전의 @ResponseBody를 각 메소드마다 선언했던 것을 한번에 사용할 수 있게 해주는 것

@GetMapping: HTTP Method인 Get의 요청을 받을 수 있는 API를 만들어 줌

이것으로 이 프로젝트는 /hello로 요청이 오면 "hello"를 반환하는 기능을 갖게 됨

 

3. HelloControllerTest.java

테스트 코드 작성은 test 디렉토리에 같은 이름의 패키지와 끝에 Test를 붙인 클래스 이름의 java 파일을 만들어 실행

package com.jojoldu.book.springboot;

import com.jojoldu.book.springboot.web.HelloController;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
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;

@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void helloIsReturn() throws Exception {
        String hello = "hello";

       mvc.perform(get("/hello"))
               .andExpect(status().isOk())
               .andExpect(content().string(hello));
    }
}

perform(get, status, content오류 생기는 분들은 import 덜해서 생긴 거니까

위 코드의 import static~으로 시작하는 저 3줄의 코드 추가해주시길!

 

@ExtendWith(SpringExtension.class): 테스트 클래스 또는 메소드의 동작을 확장하기 위한 어노테이션

옵션으로 @ExtendWith(SpringExtension.class)와 @ExtendWith(MockExtension.class)가 많이 쓰이는데 책에서는 전자를 사용한다.

SpringExtension.class는 Spring TextContext Framework를 JUnit 5의 주피터 프로그래밍 모델에 통합해서 사용한다.

MockExtension.class는 JUnit4 Mockito와 동등한 JUnit Jupiter이며, 구현하는 인터페이스가 SpringExtension.class보다 (당연하게도)적어서 Mokito와 관련된 MockContext기반에서 좀 더 가볍게 진행 가능하다.

@WebMvcTest: 여러 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션

(@Controller 등을 쓸 수 있으나 @Service, @Component, @Repository는 못 씀)

@AutoWired: 스프링이 관리하는 빈을 주입 받음

@private MockMvc mvc: 웹 API를 테스트할 때 사용. 스프링 MVC 테스트의 시작점 (이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트 가능)

@mvc.perform(get("/hello")): MockMvc를 통해 /hello 주소로 HTTP GET 요청

@.andExpect(status().isOk()): mvc.perform의 결과를 검증. HTTP Header의 Status를 검증 (200, 404, 500 등의 상태 검증)

@.andExpect(content().string(hello)): mvc.perform 결과를 검증. 응답 본문 내용을 검증. Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 검증

Run 하면 테스트 통과하는 걸 볼 수 있다.

 

이렇게 테스트 코드 통과 후! 그래도 찝찝하면 그때 Application.java 파일로 이동하여 수동으로 실행해본다.

그럼 실행 로그에 커다란 스프링 로고와 함께 잘 되었다는 것이 뜨고

아랫줄의 오른쪽 부근을 보면 톰캣 서버가 8080포트로 실행되었다는 것도 로그에 출력됨.

실행이 끝났다면 웹 브라우저를 열어서 localhost:8080/hello 로 접속!

그러면 웹 브라우저의 흰 바탕에 hello 문자열이 떠 있는 걸 확인할 수 있다.

 

Hello Controller 코드를 롬복으로 전환하기

롬복 설치는 앞에 의존성 주입할 때만 주의하면 문제없이 진행할 수 있다.

 

⚫대규모의 프로젝트였다면 이렇게 쉽게 롬복으로 전환이 가능했을까?에 대해 생각해 보기

➡️절대 쉽지 않았을 것이다. 하지만 쉽게 변경할 수 있는 이유는 테스트 코드가 우리의 코드를 지켜주기 때문이다.

롬복으로 변경 후, 문제가 생기는 부분은 테스트 코드를 돌려보면 알 수 있다!

 

롬복 전환을 위해 dto 패키지를 추가하여 파일을 만든다.

 

4. HelloResponseDto.java

package com.jojoldu.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

 

5. HelloResponseDtoTest.java

package com.jojoldu.book.springboot.web.dto;

import com.jojoldu.book.springboot.web.web.dto.HelloResponseDto;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class HelloResponseDtoTest {

    @Test
    public void lombokTest() {
        //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);
    }
}

assertThat: asserttj라는 테스트 검증 라이브러리의 검증 메소드. 

검증하고 싶은 대상을 메소드 인자로 받음.

⚫왜 Junit의 기본 assertThat이 아닌 asserttj의 assertThat인가?

: 추가적으로 CoreMatchers 라이브러리가 필요하지 않고, Matcher 라이브러리의 자동완성이 좀 더 확실하게 지원됨.

isEqualTo: assertj의 동등 비교 메소드. assertThat에 있는 값과 비교해서 같을 때만 성공

 

6. HelloController.java에 코드 추가

    @GetMapping("/hello/dto")
    public HelloResponseDto helloDto(@RequestParam("name") String name,
                                     @RequestParam("amount") int amount) {
        return new HelloResponseDto(name, amount);
    }

@RequestParam: 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션

➡️외부에서 name이란 이름으로 넘긴 파라미터를 메소드 파라미터 name (String name)에 저장하게 됨

 

name과 amount는 API를 호출하는 곳에서 넘겨준 값들이다.

 

7. HelloControllerTest.java

package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.web.web.HelloController;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
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;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.hamcrest.Matchers.is;

@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void helloReturn() throws Exception {
        String hello = "hello";

       mvc.perform(get("/hello"))
               .andExpect(status().isOk())
               .andExpect(content().string(hello));
    }

    public void helloDtoReturn() 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 응답값을 필드별로 검증할 수 있는 메소드.

$를 기준으로 필드명 명시. (여기서는 name과 amount 검증)

 

 

 

참고

https://jojoldu.tistory.com/

https://stackoverflow.com/questions/61433806/junit-5-with-spring-boot-when-to-use-extendwith-spring-or-mockito

https://velog.io/@geunwoobaek/Spring-Junit5-Test%EC%A0%95%EB%A6%AC

Comments