
레포지토리 엔티티 정의 → 컨트롤러 작성 → 서비스 작성 순서로 진행된다.
개발자가 작성한 코드가 의도한 대로 잘 동작하고, 예상치 못한 문제가 없는지 확인할 목적으로 작성한다. 테스트 코드 작성은 코드 수정 시 기존 기능이 제대로 작동하지 않을까봐 걱정하지 않아도 된다는 장점이 있다. 유지보수에 매우 좋으며, 기능 검증에 꼭 필요하다.
실제 코드에 대한 테스트 코드가 제대로 작성되어 있는지 지표를 볼 수 있다. → Test coverage(60%~)
Test coverage


테스트 코드는 test디렉터리에 작성하며, given-when-than 패턴 사용이 유명하다. 테스트 코드 연습에는 JUnit(테스트 프레임워크) 사용할 예정!
// 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절로 나누어져있다.
스프링 부트는 애플리케이션 테스트를 위한 도구와 어노테이션을 제공하며, sprint-boot-starter-test 스타터에 테스트를 위한 도구들이 모여있다.
스프링 부트 스타터 테스트 목록
- JUnit : 자바 프로그래밍 언어용 단위 테스트 프레임워크
- Spring Test & Spring Boot Test : 스프링 부트 애플리케이션을 위한 통합 테스트 지원
- AssertJ : 검증문인 assertion을 작성하는 데 사용하는 라이브러리
- Hamcrest : 표현식을 이해하기 쉽게 만드는 데 사용되는 Matcher 라이브러리
- Mockito : 테스트에 사용할 가짜 객체인 목 객체를 쉽게 만들고, 관리하고, 검증할 수 있게 지원하는 테스트 프레임워크
- JSONassert : JSON용 assertion 라이브러리
- JsonPath : JSON 데이터에서 특정 데이터를 선택하고 검색하기 위한 라이브러리
자바 언어를 위한 단위 테스트 프레임워크
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
@BeforeEach
@AfterAll
@AfterEach
어노테이션을 중심으로 JUnit의 실행 흐름은 @BeforeEach 부터 @AfterEach까지 테스트 개수만큼 반복된 결과물을 볼 수 있다.
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();
}
}
깃허브 : https://github.com/jeongggggg/spring-project-blog-api
네트워크에서 API는 프로그램간에 상호작용을 하기 위한 매개체이다.

웹 사이트 주소를 입력해서 ‘네이버 메인 화면을 보여줘’ 라고 요청하면 API는 이 요청을 받아서 서버에 가져다준다. 그럼 서버는 API가 준 요청을 처리해 결과물을 만들고 이것을 다시 API로 전달한다. 그럼 API는 최종 결과물을 브라우저에 보내주고 화면을 볼 수 있게 된다.
명확하고 이해하기 쉬운 URL로 설계를 한 API를 말한다. 보통 자원을 이름으로 URL을 짓기 때문에 명확하고 이해하기 쉬운 설계라고 할 수 있다.
✅ 규칙 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
동사(역할) -> HTTP METHOD
- GET / students
- GET / students/1
등록/추가
- POST /students
수정
- PUT /students
삭제
- DELETE /students
| 설명 | 적합한 HTTP 메소드와 URL |
|---|---|
| id가 1인 블로그 글을 조회하는 API | GET /articles/1 |
| 블로그 글을 추가하는 API | POST /articles |
| 블로그 글을 수정하는 API | PUT /articles/1 |
| 블로그 글을 삭제하는 API | DELETE /articles/1 |
슬래시는 계층 관계를 나타내는 데 사용하거나, 밑줄 대신 하이픈을 사용하거나, 자원의 종류가 컬렉션인지 도큐먼트인지에 따라 단수, 복수를 나누거나 하는 등의 규칙도 있다.
logback-local.xml 설정
error 레벨까지만 출력하도록 설정할 수 있다.warn: warn과 error 로그만 출력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을, 자바에서는 객체를 사용하지만, 서로 형식이 다르기 때문에 형식에 맞게 변환하는 작업이 필요하다. 이런 작업을 직렬화, 역직렬화라고 한다.

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