[Spring Boot] 테스트코드 작성 방법, API와 REST API

dejeong·2024년 10월 15일
0

DBMS

목록 보기
9/10
post-thumbnail

레포지토리 엔티티 정의 → 컨트롤러 작성 → 서비스 작성 순서로 진행된다.

Spring Boot 테스트

테스트 코드 개념

개발자가 작성한 코드가 의도한 대로 잘 동작하고, 예상치 못한 문제가 없는지 확인할 목적으로 작성한다. 테스트 코드 작성은 코드 수정 시 기존 기능이 제대로 작동하지 않을까봐 걱정하지 않아도 된다는 장점이 있다. 유지보수에 매우 좋으며, 기능 검증에 꼭 필요하다.

실제 코드에 대한 테스트 코드가 제대로 작성되어 있는지 지표를 볼 수 있다. → Test coverage(60%~)

Test coverage

테스트 코드는 test디렉터리에 작성하며, given-when-than 패턴 사용이 유명하다. 테스트 코드 연습에는 JUnit(테스트 프레임워크) 사용할 예정!

  1. given: 테스트 실행을 위해 필요한 데이터를 준비하는 단계
  2. when: 테스트를 진행하는 단계
  3. then: when에서 수행한 테스트 결과를 검증하는 단계
// assertThat 메소드 사용을 위한 static import
import static org.assertj.core.api.Assertions.*;

// 새로운 메뉴를 저장하는 코드 테스트
@DisplayName("새로운 메뉴를 저장한다.")
@Test
public void saveMenuTest() {
		// given : 메뉴를 저장하기 위한 준비 과정
		String name = "카페라떼";
		int price = 5500;
		
		Menu latte = new Menu(name, price);
		
		// when : 실제로 메뉴를 저장
		long saveId = menuService.save(latte);
		
		// then : 메뉴가 잘 추가되었는지 검증 
		Menu savedMenu = menuService.findById(saveId).get();
		assertThat(savedMenu.getName()).isEqualTo(name);
		assertThat(savedMenu.getPrice()).isEqualTo(price);		
}

assertThat 은 AssertJ 테스트 코드 라이브러리에 들어있는 메소드로 AssertJ 를 import 해주어야 한다. 메뉴를 저장하기 위해 준비하는 과정인 given절, 실제로 메뉴를 저장하는 when절, 메뉴가 잘 추가되었는지 검증하는 then절로 나누어져있다.

스프링 부트3와 테스트

스프링 부트는 애플리케이션 테스트를 위한 도구와 어노테이션을 제공하며, sprint-boot-starter-test 스타터에 테스트를 위한 도구들이 모여있다.

스프링 부트 스타터 테스트 목록

  • JUnit : 자바 프로그래밍 언어용 단위 테스트 프레임워크
  • Spring Test & Spring Boot Test : 스프링 부트 애플리케이션을 위한 통합 테스트 지원
  • AssertJ : 검증문인 assertion을 작성하는 데 사용하는 라이브러리
  • Hamcrest : 표현식을 이해하기 쉽게 만드는 데 사용되는 Matcher 라이브러리
  • Mockito : 테스트에 사용할 가짜 객체인 목 객체를 쉽게 만들고, 관리하고, 검증할 수 있게 지원하는 테스트 프레임워크
  • JSONassert : JSON용 assertion 라이브러리
  • JsonPath : JSON 데이터에서 특정 데이터를 선택하고 검색하기 위한 라이브러리

JUnit

자바 언어를 위한 단위 테스트 프레임워크

  • 단위 테스트 : 작성한 코드가 의도대로 동작하는지 작은 단위로 검증하는 것으로 이때 단위는 보통 메소드가 된다.

JUnit의 특징

  • 테스트 방식을 구분할 수 있는 어노테이션 제공
    • @Test 어노테이션으로, 메소드를 호출할 때마다 새 인스턴스를 생성, 독립테스트 가능
  • 예상 결과를 검증하는 Assertion 메소드 제공
  • 사용 방법이 단순, 테스트 코드 작성 시간이 적음
  • 자동 실행, 자체 결과를 확인하고 즉각적인 피드백을 제공

⭐ Test 클래스 네이밍은 클래스명 뒤에 Test 를 붙여준다. (JUnitTest)

package com.estsoft.springproject;

import org.junit.jupiter.api.Test;

public class JUnitTest {
    @Test
    public void test() {
        // given
        // when
        // then
    }
}

보통 위와 같은 구문으로 작성하며, 테스트할 부분이 없다면 생략되는 구문이 있기도 하다.

package com.estsoft.springproject;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class JUnitTest {
    @Test
    public void test() {
        // given
        int a = 1;
        int b = 2;

        // when : 검증하고 싶은 메소드(코드) 호출
        int sum = a + b;

        // then : when절 실행한 결과 검증
        Assertions.assertEquals(3, sum); // 기대되는 값, 실제 로직을 돌려본 결과
    }
}

Assertions 메소드를 사용하여 검증한다. (Assertion은 기댓값과 실제값을 명시하지 않으므로, 비교 대상이 헷갈리는 단점이 있다. → 아래에서 AssertJ 사용 예정)

junit, assertj 둘 다 많이 사용되지만 위 테스트에서는 junit 사용!

메소드 오버로딩이 되어 있는데, 각각의 메소드가 전달받는 인자의 타입이 다르기 때문에 확인 후 사용할 인자에 맞는 값으로 테스트 진행

테스트 성공시

테스트 실패시 - 에러 메세지 확인 후 수정 필요

컨트롤러 코드를 테스트하기위해 main > controller 에서 ctrl + shift + T 또는 alt + enter을 하면

자동으로 테스트 코드 생성을 진행할 수 있다.

package com.estsoft.springproject.controller;

import com.estsoft.springproject.repository.MemberRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.junit.jupiter.api.Assertions.*;

// 테스트를 위해 실제 서버가 뜬 것처럼 하기 위해 추가(테스트 서버)  (when 절을 위함)
@SpringBootTest // @SpeingBootApplication 을 붙인 클래스를 찾아서 테스트와 관련된 bean 들을 넣어준다. (when 절을 위함)
@AutoConfigureMockMvc
class MemberControllerTest {
    // 테스트 관련 세팅(테스르를 위한 관련된 bean 주입) (when 절을 위함)
    @Autowired
    WebApplicationContext webApplicationContext;

    @Autowired
    MockMvc mockMvc;

    @Autowired // given 테스트를 위한 실제 레포지토리 의존성 주입
    MemberRepository memberRepository;

    @BeforeEach
    public void setUp(){
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void testGetAllMember() {
        // given : 멤버 목록 저장
        // when : GET /members
        // then : response 검증
    }
}

// 단위 테스트 보다는 통합 테스트에 더 가깝다.

package com.estsoft.springproject.controller;

import com.estsoft.springproject.repository.MemberRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

// 테스트를 위해 실제 서버가 뜬 것처럼 하기 위해 추가(테스트 서버)  (when 절을 위함)
@SpringBootTest // @SpeingBootApplication 을 붙인 클래스를 찾아서 테스트와 관련된 bean 들을 넣어준다. (when 절을 위함)
@AutoConfigureMockMvc
class MemberControllerTest {
    // 테스트 관련 세팅(테스르를 위한 관련된 bean 주입) (when 절을 위함)
    @Autowired
    WebApplicationContext webApplicationContext;

    @Autowired
    MockMvc mockMvc;

    @Autowired // given 테스트를 위한 실제 레포지토리 의존성 주입
    MemberRepository memberRepository;

    @BeforeEach
    public void setUp(){
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void testGetAllMember() throws Exception {
        // given : 멤버 목록 저장  (생략)

        // when : GET /members
        ResultActions resultActions = mockMvc.perform(get("/members").accept(MediaType.APPLICATION_JSON)); // accept(MediaType.APPLICATION_JSON) -> 요청에 대한 응답은 JSON 형태로 받겠다.

        // then : response 검증
        resultActions.andExpect(status().is2xxSuccessful())
                .andExpect(jsonPath("$[0].id").value(1)) // id 값에 대한 검증, given 절에서 특정 값을 할당해주었다면 value에 할당한 값을 넣어주어도 된다.
                .andExpect(jsonPath("$[1].id").value(2))
        ;
    }
}

Spring MVC : 클라이언트 요청을 받고, 처리, 응답하는 모듈로 DispatcherServlet 기반으로 동작한다.

import org.junit.jupiter.api.*;

public class JUnitTotalTest {		
		@BeforeAll					// 전체 테스트를 시작하기 전 1회 실행
		public static void beforeAll() {
				System.out.println("@BeforeAll");
		}
		
		@BeforeEach					// 각각의 테스트 케이스를 실행하기 전마다 실행
		public void beforeEach() {
				System.out.println("@BeforeEach");
		}
		
		@Test
		public void test1() {
				System.out.println("test1");
		}
	
		@Test
		public void test2() {
				System.out.println("test2");
		}
	
		@Test
		public void test3() {
				System.out.println("test3");
		}
		
		@AfterAll						// 전체 테스트를 마치고 종료하기 전에 1회 실행
		public static void afterAll() {
				System.out.println("@AfterAll");
		}
		
		@AfterEach						// 각각의 테스트 케이스를 종료하기 전마다 실행
		public void afterEach() {
				System.out.println("@AfterEach");
		}
}

  • @BeforeAll

    • 전체 테스트를 시작하기 전에 처음으로 한 번만 실행한다.
    • 데이터베이스를 연결해야 하거나 테스트 환경을 초기화할 때 사용한다.
    • 전체 테스트 실행 주기에서 한 번만 호출되어야 하기 때문에 메소드를 static으로 선언한다.
  • @BeforeEach

    • 테스트 케이스를 시작하기 전에 매번 실행한다.
    • 테스트 메소드에서 사용하는 객체를 초기화하거나 테스트에 필요한 값을 미리 넣을 때 사용할 수 있다.
    • 각 인스턴스에 대해 메소드를 호출해야하므로 메소드는 static이 아니어야 한다.
  • @AfterAll

    • 전체 테스트를 마치고 종료하기 전에 한 번만 실행한다.
    • 데이터베이스 연결을 종료할 때나 공통적으로 사용하는 자원을 해제할 때 사용할 수 있다.
    • 전체 테스트 실행주기에서 한 번만 호출되어야 하므로 메소드를 static으로 선언해야 한다.
  • @AfterEach

    • 각 테스트 케이스를 종료하기 전 매번 실행한다.
    • 테스트 이후에 특정 데이터를 삭제해야 하는 경우 사용한다.
    • @BeforeEach 어노테이션과 마찬가지로 메소드는 static이 아니어야 한다.

어노테이션을 중심으로 JUnit의 실행 흐름은 @BeforeEach 부터 @AfterEach까지 테스트 개수만큼 반복된 결과물을 볼 수 있다.

AssertJ

JUnit과 함께 사용해 검증문의 가독성을 높여주는 라이브러리

메소드 이름설명
isEqualTo(A)A 값과 같은지 검증
isNotEqualTo(A)A 값과 다른지 검증
contains(A)A 값을 포함하는지 검증
doesNotContains(A)A 값을 포함하지 않는지 검증
startsWith(A)A 값으로 시작하는지 검증
endsWith(A)A 값으로 끝나는지 검증
isEmpty()비어있는 값인지 검증
isNotEmpty()비어있지 않은 값인지 검증
isPositive()양수인지 검증
isNegative()음수인지 검증
isGreaterThan(1)1보다 큰 값인지 검증
isLessThan(1)1보다 작은 값인지 검증
// Assertion 
Assertions.assertEquals(sum, a + b);

// AssertJ
assertThat(a + b).isEqualTo(sum);
package com.estsoft.springproject;

import org.assertj.core.api.Assert;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class JUnitTest {
    @Test
    public void test() {
        int a = 1;
        int b = 2;

        int sum = a + b;

        //Assertions.assertEquals(3, sum);
        Assertions.assertThat(sum).isEqualTo(3);
        Assertions.assertThat(sum).isOdd();
    }
}

Spring Boot로 블로그 만들기

깃허브 : https://github.com/jeongggggg/spring-project-blog-api

API와 REST API

네트워크에서 API는 프로그램간에 상호작용을 하기 위한 매개체이다.

웹 사이트 주소를 입력해서 ‘네이버 메인 화면을 보여줘’ 라고 요청하면 API는 이 요청을 받아서 서버에 가져다준다. 그럼 서버는 API가 준 요청을 처리해 결과물을 만들고 이것을 다시 API로 전달한다. 그럼 API는 최종 결과물을 브라우저에 보내주고 화면을 볼 수 있게 된다.

REST API (Representational State Transfer)

명확하고 이해하기 쉬운 URL로 설계를 한 API를 말한다. 보통 자원을 이름으로 URL을 짓기 때문에 명확하고 이해하기 쉬운 설계라고 할 수 있다.

  • 장점
    • URL만 보고도 무슨 행동을 하는 API인지 명확하게 알 수 있다.
    • 상태가 없다는 특징이 있어서 클라이언트와 서버의 역할이 명확하게 분리된다.
    • HTTP 표준을 사용하는 모든 플랫폼에서 사용할 수 있다.
    • ⭐ 주소와 메소드만 보고 요청의 내용을 파악할 수 있다.
  • 단점
    • HTTP 메소드 즉 GET, POST 와 같은 방식의 개수에 제한이 있다.
    • 설계를 위해 공식적으로 제공되는 표준 규약이 없다.

REST API 사용 방법

규칙 1. URL에는 동사를 쓰지 말고, 자원을 표시해야 한다.

자원은 가져오는 데이터를 말한다. id가 1인 학생의 정보를 가져오는 URL 설계는

/students/1 // -> REST API에 더 맞는 RESTful한 API

/get-student?student_id=1 // -> 자원이 아닌 다른 표현을 섞어 사용했기 때문 (동사를 사용해서 추후 개발시 혼란을 줄 수 있다.)

서버에서 데이터를 요청하는 URL을 설계할 때 어떤 개발자는 get을, 어떤 개발자는 show를 쓰면 URL의 구조가 get-student, show-student와 같이 각자 다른 명칭이 오게된다. 행위는 ‘데이터를 가져온다’지만 표현이 중구난방이 되기 때문에 RESTful API를 설계할 때는 동사를 쓰지 않는다.

예시 API적합성설명
/articles/1적합동사없음, 1번 글을 가져온다는 의미가 명확

| /articles/show/1
/show/articles/1 | 부적합 | show 라는 동사가 있어 부적합 |

규칙2. 동사는 HTTP 메소드로

CRUD

  • POST : 만들고 (Create)
  • GET : 읽고 (Read)
  • PUT : 업데이트 하고 (Update)
  • DELETE : 삭제하고 (Delete)
동사(역할) -> HTTP METHOD
- GET / students
- GET / students/1

등록/추가
- POST /students

수정
- PUT /students

삭제
- DELETE /students
설명적합한 HTTP 메소드와 URL
id가 1인 블로그 글을 조회하는 APIGET /articles/1
블로그 글을 추가하는 APIPOST /articles
블로그 글을 수정하는 APIPUT /articles/1
블로그 글을 삭제하는 APIDELETE /articles/1

슬래시는 계층 관계를 나타내는 데 사용하거나, 밑줄 대신 하이픈을 사용하거나, 자원의 종류가 컬렉션인지 도큐먼트인지에 따라 단수, 복수를 나누거나 하는 등의 규칙도 있다.

블로그 글 작성을 위한 API 구현

logback-local.xml 설정

  • 로그 레벨을 조정하여 로그 출력 범위를 관리할 수 있다.
  • 예를 들어, 특정 서버 로그는 error 레벨까지만 출력하도록 설정할 수 있다.
  • 로그 레벨별 출력 내용:
    • warn: warnerror 로그만 출력
    • debug: debug, info, warn, error 로그 모두 출력

로깅

로그를 사용하면 System.out을 통한 출력보다 더 유용하며, 로그를 통해 출력 결과를 확인할 수 있다. 또한, 콘솔창에만 로그를 남기는 것이 아니라, 서버에 로그를 저장하여 나중에 확인할 수 있는 방법도 제공된다.

@RestController
public class BlogController {
    @PostMapping("/articles")
    public ResponseEntity<Article> writeArticle(@RequestBody AddArticleRequest request){
        System.out.println(request.getTitle());
        System.out.println(request.getContent());
      
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}
@Slf4j // 로깅 어노테이션
@RestController
public class BlogController {
    @PostMapping("/articles")
    public ResponseEntity<Article> writeArticle(@RequestBody AddArticleRequest request){
        // logging
        // 로깅 레벨은 trace, debug, info, warn, error
        log.debug("{}, {}",request.getTitle(), request.getContent());

        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}

개발자가 확인하는 용도임으로 로컬 저장소에 올릴 때에는 지우고 올려야 하지만, 서버 안에서 확인이 필요한 로그는 남긴다. ⇒ 중요한 비즈니스 로직일 때(권장하지는 않음)

로그를 남기는 기준은 다르지만 조직에서 반드시 확인이 되어야할 비즈니스 로직, 값 등을 서버에서 확인하고 싶을 때 로그로 남긴다.

요청이 들어왔을 때 요청에 대한 흐름을 확인하기 위해서 트랜잭션 id를 txId 를 남긴다. (txId)

// 요청 txId : 1
log.warn("반드시 확인해야 하는 값 : {}", 1));

자바에서의 직렬화, 역직렬화

HTTP에서는 JSON을, 자바에서는 객체를 사용하지만, 서로 형식이 다르기 때문에 형식에 맞게 변환하는 작업이 필요하다. 이런 작업을 직렬화, 역직렬화라고 한다.

  • 직렬화 : 자바 시스템 내부에서 사용되는 객체를 외부에서 사용하도록 데이터를 변환하는 작업으로 “title”은 “제목”, “content”는 “내용” 이라는 값이 들어있는 객체가 있다고 가정했을 때, 이때 이 객체를 JSON 형식으로 직렬화할 수 있다.
  • 역직렬화 : 직렬화의 반대로 외부에서 사용하는 데이터를 자바의 객체 형태로 변환하는 작업을 말하며, JSON 형식의 값을 자바 객체에 맞게 변환하는 것을 말한다.
Given블로그 글 생성에 필요한 요청 객체를 만든다.
When블로그 글 생성 API를 호출합니다. 이때 요청 타입은 JSON
Then응답코드가 201 Created로 정상적으로 들어왔는지를 확인 실제로 저장된 데이터와 요청 값을 비교한다.
@AutoConfigureMockMvc
@SpringBootTest
class BlogControllerTest {
    @Autowired
    private WebApplicationContext context;

    @Autowired
    private MockMvc mockMvc;

    @Autowired // 의존성 주입
    ObjectMapper objectMapper;

    @Autowired
    private BlogRepository blogRepository;

    @BeforeEach
    public void setUp(){
        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

    // POST /articles API 테스트
    @Test
    public void addArticle() throws Exception {
        // given : article 저장
        // Article article = new Article("제목","내용");
        AddArticleRequest request = new AddArticleRequest("제목","내용");
        // blogRepository.save(article); => 삭제
        // 직렬화 (article object를 json 형태로, writeValueAsString 직렬화 메소드)
        String json = objectMapper.writeValueAsString(request);

        // when : POST /articles API 호출
        ResultActions resultActions = mockMvc.perform(post("/articles")
                .contentType(MediaType.APPLICATION_JSON) // Content-Type을 JSON으로 설정
                .content(json)); // JSON 데이터를 요청의 본문에 추가

        // then : 호출 결과 검증
        resultActions.andExpect(status().isCreated())
                .andExpect(jsonPath("title").value(request.getTitle()))
                .andExpect(jsonPath("content").value(request.getContent()));

        List<Article> articleList = blogRepository.findAll();
				assertions.assertThat(articleList.size()).isEqualTo(1);
    }
}
profile
룰루

0개의 댓글