Loading...

Spring / / 2022. 5. 11. 12:31

스프링부트 Junit5 통합 테스트

반응형

< 스프링 구조 >

 

Layer를 분리해놓은 이유 : 책임 분리

 

 

 

컨트롤러가 잘 실행하려면 레이어가 다 메모리에 떠야 한다.

무거울 것이다.

 

이는 메인을 실행하는 것과 마찬가지이다.

 

보통 메인으로는 테스트를 잘하지 않는다.

메인으로 실행한다는 것은 전체 프로젝트를 띄우는 것이기 때문이다.

 

전체 프로그램을 메모리에 띄우고 테스트하는 것을 통합 테스트라고 한다.

 

@SpringBootTest // 통합테스트 -> 이 파일이 실행될 때 모든게 메모리에 다 뜸
public class NaverApiControllerTest {

}

 

내가 윈도우에서 개발한 프로젝트를 AWS로 던져서 gradle로 빌드할 때

빌드 과정에서 test 파일을 무조건 실행한다.

테스트가 잘 실행되면 jar 파일로 구워준다.

 

만약 테스트가 실패하면 github에 코드를 다시 push 해서 clone 받아야 한다.

 

애초에 로컬 환경과 AWS 환경이 다르기 때문에 테스트에 어려움이 있을 것이다.

 

그래서 제일 좋은 것은 같은 환경에서 개발하는 게 좋지만,

그게 안된다면 도커로 환경을 만들어 개발하는게 좋다.

 

도커는 가상 OS환경이다.

 

이렇게 전체 통합 테스트를 한다는 것은 테스트 파일을 만든다는 것이다.

테스트 파일을 만들어 배포 환경에서 돌려보고 잘되면 jar 파일로 굽는다.

 

이게 배포 로직이다.

 


 

그럼 단위 테스트는 무엇일까?

부분 테스트를 말한다.

 

전체 프로젝트를 메모리에 띄우지 않고 부분만 메모리에 띄워서 테스트하는 것을 단위 테스트라고 한다.

여기서 부분은 레이어의 부분을 말한다.

 

단위 테스트의 장점 : 메모리에 부분만 뜨기 때문에 가볍다.

 

해당 레이어의 책임만 테스트하겠다는 것이다.

 

만약 컨트롤러를 메모리에 띄운다는 것은 컨트롤러 앞단은 다 메모리에 띄우겠다는 것이다.

 

컨트롤러를 테스트할 때 DB에서 데이터를 받아올 수 없기 때문에

가짜 데이터를 만들어서 사용한다.

 

DB 데이터를 테스트하려는 게 아니니까!

 

컨트롤러를 테스트할 때 서비스에 의존적인 부분이 있다면

의존성을 모두 끊어내고 가짜 서비스를 만들어 테스트해야 한다.

 

여기서 이 가짜 서비스를 Mock라고 한다.

 

다른 레이어에 있는 메서드와 연결되어 있을 때

가짜 메서드를 만드는 게 어렵다ㅜㅜ

 


 

컨트롤러 테스트를 해볼 건데

통합 테스트를 하기 위한 NaverApiControllerTest.java 파일을 만들어주자.

 

given (가짜 데이터 만들기) - when (실행) - then (검증)

 

테스트 할 때는 다음과 같은 틀을 가진다.

 

 

 

클래스에 @SpringbootTest 어노테이션을 붙여주면

이 파일이 실행될 때 메모리에 전체 레이어가 올라가면서 통합 테스트 할 수 있다.

 

단위 테스트를 할 때는 이 어노테이션을 붙여주면 안 된다!!

 

통합 테스트 할 때는 전체 클래스를 실행시켜줘야 한다.

메서드에서 초록색 화살표 버튼을 클릭하지 않는다!

 

8080 포트가 이미 돌고 있을 수도 있으니까 이 파일만의 포트를 지정해주는 게 좋다.

랜덤으로 남는 포트를 지정해준다.

 

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)

 

컨트롤러 테스트를 하려면 http 통신을 위해 TestRestTemplate가 필요하다.

일반적으로 쓰는 RestTemplate가 아닌 테스트 전용 RestTemplate이다.

 

RestTemplate를 DI 해주기 위해 @RequiredArgsConstructor 롬복을 사용할 수 없다.

 

Junit5가 스스로 DI를 지원해주기 때문인데

이때 DI를 지원해주는 타입이 정해져 있다.

 

Junit5에서 생성자나 롬복 방식으로 DI가 안 되는 이유는

Junit이 생성자에 다른 의존성을 주입해주려고 먼저 개입을 하기 때문이다.

 

DI를 위해 롬복을 사용하지 않고 @Autowired 어노테이션을 써주자.

 

일반적인 컨트롤러에도 사용 가능한 어노테이션이다.

롬복 때문에 잘 사용하진 않지만 옛날에는 많이 사용했었다.

 

테스트 할 때는 @Autowired를 사용해주자.

 

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) // 통합테스트 -> 이 파일이 실행될 때 모든게 메모리에 다 뜸 -> 시간 좀 걸림
public class NaverApiControllerTest {
    @Autowired // DI 어노테이션
    private TestRestTemplate rt; // http 통신 -> 컨트롤러 때리기
}

 

 

 

RestTemplate에 getForObject나 getForEntity와 같은 메서드로 통신을 했는데

이를 사용하면 Get 요청에는 get관련 메서드, Post 요청에는 post 관련 메서드로

바꿔서 사용해줘야 했었다.

 

exchange라는 통합된 메서드를 RestTemplate가 제공해준다.

매개변수로 HttpMethod를 전송할 수 있기 때문이다.

 

컨트롤러는 응답을 json으로 해주니까 String으로 받으면 된다.

 

근데 내가 지금 save를 할 건데 포스트맨으로 테스트할게 아니니까

컨트롤러로 던질 데이터를 만들어줘야 한다.

 

@Test
public void save_테스트() {
    // given (가짜 데이터 만들기)
    Naver naver = new Naver(); // json으로 바꿔서 전송
    naver.setTitle("스프링1강");
    naver.setCompany("재밌어요");
}

 

데이터를 만들어서 json으로 바꿔준 뒤 컨트롤러에 던져야 한다.

ObjectMapper를 사용하자.

ObjectMapper om = new ObjectMapper(); // 바이트도 오브젝트로 바꿔줌. Gson 보다 많은 기능을 제공함.
String content = om.writeValueAsString(naver); // 오브젝트 -> json 변환

 

ObjectMapper의 기능

오브젝트 → json 변환 메서드 om.writeValueAsString( )

json → 오브젝트 변환 메서드 om.readValue( )

 

* 바이트 → 오브젝트 *

원래 바이트를 버퍼로 읽어서 바꿔줘야 하는데

바로 오브젝트로 바꿔줄 수도 있다.

 

objectMapper가 좀 더 많은 기능을 제공해준다.

 


 

이제 json으로 바꾼 데이터를 exchange 메서드에 body 매개변수로 넘겨주면 될 것 같았는데

무조건 HttpEntity <?> 타입으로만 넘겨줄 수 있다.

 

헤더가 필요하기 때문이다.

 

그냥 String으로만 던지면 application/json 타입인지 모르기 때문이다.

 

객체를 만들어주자.

// 헤더도 넣어줘야하고 하니까 객체를 만들어서 content를 담아 전송해야함
HttpEntity<String> httpEntity = new HttpEntity<>(content, headers);

// when (실행)
ResponseEntity<String> response = rt.exchange("/navers", HttpMethod.POST, httpEntity, String.class);

 

headers 자리에는 헤더 객체를 만들어줘야 한다.

근데 이 헤더는 이 메서드에만 필요한 게 아니라 다른 메서드에서도 재사용할 수 있기 때문에

init 메서드를 만들어 재사용해주자.

 

HttpHeaders는 두 가지 객체가 있는데 스프링의 헤더임을 잘 체크하자.

IoC 컨테이너에 등록할 수 있는 게 아니니까 직접 new 해줘야 한다.

 

private static HttpHeaders headers;

@BeforeAll // 이 파일이 실행되기 직전 최초에 실행 -> static 붙어야함 (통합테스트니까 하나씩 실행안할거야!)
public static void init() {
    // assertNotNull(rt); // rt가 null이 아니면 true
    headers = new HttpHeaders(); // 재사용하기 위해 init에 생성
    headers.setContentType(MediaType.APPLICATION_JSON);
}

 

이 파일이 실행될 때 최초에 무조건 실행하라는 어노테이션이 @BeforeAll 이다.

@BeforeEach도 있는데 이 둘의 차이점은

All은 여러개의 메서드가 실행되기 전에 한번 실행하는 것이고,

Each는 인터셉터처럼 각각의 메서드가 실행하기 전에 한 번씩 계속 실행되는 것이다.

 


 

통신을 하기 위해 트라이 캐치를 걸어줘야 하는데

해당 메서드의 부분적인 코드만 트라이를 걸어줄 때가 있다.

 

이때 메서드 전체에 트라이 캐치를 걸어줄 때는 메서드 시작부터 트라이 캐치로 묶어줘도 되지만

메서드에 throw를 걸어주는 게 더 쉽다.

대신 캐치 처리를 여기서 할 수 없다.

 

save_테스트 메서드를 호출한 쪽에서 트라이 캐치로 묶어서 처리해줘야 한다.

여기서는 Junit이 이 메서드를 호출하기 때문에 신경 쓸 필요 없다.

알아서 해줄 것이다.

 

@Test
public void save_테스트() throws JsonProcessingException { // 메서드 전체 트라이캐치(캐치 처리안됨, 호출한 쪽에서 캐치 처리해야함)
    
}

 

이제 값이 잘 들어갔는지 response의 body 데이터와 비교하여 검사해보자.

title이 "스프링1강"과 같은지 비교하면 끝난다.

 

json 데이터를 리턴 받기 때문에 JsonPath라는 라이브러리를 사용할 것이다.

키 값을 찾아서 검색하기 위해서이다.

 

// 키값을 찾아서 문자열 비교 검증. 다시 json을 오브젝트로 변환하기 귀찮으니까 이거 사용
DocumentContext dc = JsonPath.parse(response.getBody());

// 키값 찾는 방법 junit5 jsonpath 문법
String title = dc.read("$.title");

assertEquals("스프링1강", title);

 

junit5 JsonPath 문법

연산자 설명
$ 모든 Path 표현식의 시작, 루트 노드로 부터 시작하는 기호
@ 처리되고 있는 현재 노드를 나타내고 필터 조건자에서 사용
* 와일드카드, 모든 요소와 매칭
. Dot 표현식의 자식 노드
[start:end] 배열 slice 연산자
[?(<expression>)] 필터 표현식으로 필터 조건자가 참인 경우에 매칭되는 모든 요소들 만을 처리한다.
ex) book[?(@.price == 49.99)]

 


 

그 다음 검증을 위해 assert를 사용한다.

얘를 통해 테스트가 정상적으로 완료했는지 초록색 체크 모양이 뜬다.

 

assertEquals는 보통 문자열 비교할 때 사용하고

assertNull과 assertNotNull을 자주 사용한다.

참고해두자.

 

package site.metacoding.mongocrud.web;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

import site.metacoding.mongocrud.domain.Naver;

// @RequiredArgsConstructor // 사용못함, 테스트할때는 @Autowired 사용하자
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) // 통합테스트 -> 이 파일이 실행될 때 모든게 메모리에 다 뜸 -> 시간 좀 걸림
public class NaverApiControllerTest {

    @Autowired // DI 어노테이션
    private TestRestTemplate rt; // http 통신 -> 컨트롤러 때리기
    private static HttpHeaders headers;

    @BeforeAll // 이 파일이 실행되기 직전 최초에 실행 -> static 붙어야함 (통합테스트니까 하나씩 실행안할거야!)
    public static void init() {
        // assertNotNull(rt); // rt가 null이 아니면 true
        headers = new HttpHeaders(); // 재사용하기 위해 init에 생성
        headers.setContentType(MediaType.APPLICATION_JSON);
    }

    @Test
    public void save_테스트() throws JsonProcessingException { // 메서드 전체 트라이캐치(캐치 처리안됨, 호출한 쪽에서 캐치 처리해야함)
        // given (가짜 데이터 만들기)
        Naver naver = new Naver(); // json으로 바꿔서 전송
        naver.setTitle("스프링1강");
        naver.setCompany("재밌어요");

        ObjectMapper om = new ObjectMapper(); // 바이트도 오브젝트로 바꿔줌. Gson 보다 많은 기능을 제공함.
        String content = om.writeValueAsString(naver); // 오브젝트 -> json 변환

        // 헤더도 넣어줘야하고 하니까 객체를 만들어서 content를 담아 전송해야함
        HttpEntity<String> httpEntity = new HttpEntity<>(content, headers);

        // when (실행)
        ResponseEntity<String> response = rt.exchange("/navers", HttpMethod.POST, httpEntity, String.class);

        // then (검증)

        // 키값을 찾아서 문자열 비교 검증. 다시 json을 오브젝트로 변환하기 귀찮으니까 이거 사용
        DocumentContext dc = JsonPath.parse(response.getBody());
        // System.out.println(dc.jsonString());

        // 키값 찾는 방법 junit5 jsonpath 문법
        String title = dc.read("$.title");
        // System.out.println(title);

        assertEquals("스프링1강", title);
    }
}

 

똑같은 방법으로 findAll 컨트롤러 테스트도 가능하다.

 

 @Test
public void findAll_테스트() {
    // given (SELECT라서 줄 데이터가 없다)

    // when (실행)
    ResponseEntity<String> response = rt.exchange("/navers", HttpMethod.GET, null, String.class);

    // then

    // DocumentContext dc = JsonPath.parse(response.getBody());
    // String title = dc.read("$.[0].title");
    // findAll은 상태코드 확인해주자. 배포했을 때 그쪽에 데이터 없을수도 있으니까!
    // assertEquals("지방선거 6.1이 곧 다가온다.", title);
    assertTrue(response.getStatusCode().is2xxSuccessful());
}

 

 

 

[출처]

 

https://cafe.naver.com/metacoding

 

메타코딩 : 네이버 카페

코린이들의 궁금증

cafe.naver.com

메타 코딩 유튜브

https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9

 

메타코딩

문의사항 : getinthere@naver.com 인스타그램 : https://www.instagram.com/meta4pm 깃헙 : https://github.com/codingspecialist 유료강좌 : https://www.easyupclass.com

www.youtube.com

 
반응형