임도현의 성장
[Spring-Boot] JUnit & Mockito기반테스트 코드 작성 본문
📌테스트 코드 왜 작성해야 해?
축구 게임을 만들었다고 생각해 보자, 우리 공격수가 상대방 골대에 골을 넣었는데 우리팀 점수가 올라가야 하는데 상대팀 점수가 올라가는 버그가 생겼어 이 문제를 수정하기 위해 매번 직접 실행해서 확인 하면 시간도 많이 걸리고, 실수도할 수 있잖아 이때 테스트 코드는 한번의 실행으로 우리팀 점수가 올랐는지 확인 할 수 있고, 리팩토링이나 코드를 수정했을 때 동작이 잘 됬는지 확인도 할 수 있어
😧테스트 코드의 특징
- 버그 예방 및 조기 발견: 개발 중 작성한 코드가 예상대로 작동하는지 즉시 확인할 수 있으며, 버그를 조기에 발견하고 수정할 수 있습니다.
- 리팩토링의 안전성 확보: 코드 리팩토링을 할 때 기존 기능의 동작이 변하지 않았는지 확인할 수 있어, 코드 변경이 다른 부분에 미치는 영향을 최소화할 수 있습니다.
- 문서화 역할: 단위 테스트는 코드의 사용법을 명확히 설명하는 역할을 합니다. 테스트 메서드는 메서드를 어떻게 사용해야 하는지, 어떤 입력이 필요하며 어떤 결과가 나와야 하는지를 명확히 보여줍니다.
- 자동화된 테스트: 단위 테스트는 일반적으로 자동으로 실행되며, CI/CD 파이프라인에 통합되어 코드 변경 시마다 자동으로 테스트가 실행됩니다. 이를 통해 변경된 코드가 예상대로 동작하는지 확인할 수 있습니다.
🌐JUnit란?
JUnit는 Java에서 가장 널리 사용되는 테스트 프레임워크로, 단위 테스트를 작성하고 실행하는 데 사용됩니다. JUnit은 테스트 메서드 작성, 실행, 결과 보고 등의 기능을 제공합니다. 특히 테스트 코드의 구조를 잘 정의할 수 있게 도와주고, 테스트 결과를 쉽게 확인할 수 있도록 합니다.
🚀단위 테스트란?
단위 테스트(Unit Test)는 소프트웨어 개발에서 개별적인 함수나 메서드 등의 "단위"가 올바르게 동작하는지 확인하기 위해 작성하는 테스트 코드입니다. 주로 프로그램의 각 기능을 독립적으로 검증하여, 각 부분이 제대로 작동하는지 확인하는 데 사용됩니다. 단위 테스트는 보통 개발자가 코드를 작성하면서 병행하여 작성합니다.
🍸Mockito란?
Mockito는 실제 객체를 사용하기 어려운 상황에서 "가짜 객체"(mock 객체)를 생성하여, 서비스나 레포지토리와 같은 의존성을 테스트 대상 객체에 주입하는 역할을 합니다. 이렇게 하면 실제 객체 없이도 컨트롤러, 서비스, 레포지토리 등 여러 계층의 동작을 독립적으로 테스트할 수 있습니다.
💽단위 테스트 작성 기준
- 단위 테스트는 JUnit과 assertThat 조합을 사용합니다.
- Given/When/Then 패턴
- Given : 어떠한 데이터가 주어질 때.
- When : 어떠한 기능을 실행하면.
- Then : 어떠한 결과를 기대한다.
@DisplayName("")
@Test
void test() {
// given
// when
// then
}
🙊테스트 코드 작성을 위한 준비
- CreateMember : 회원 정보를 받아 새 회원을 생성하고, 생성된 회원 정보를 반환합니다.
- SelectMember : 전달 받은 id 값을 이용해 특정 회원 정보를 조회하고 반환합니다.
@RequiredArgsConstructor
@RestController
public class MemberController {
private final MemberService memberService;
@PostMapping("/member")
public ApiResponse<MemberResponse> CreateMember(@Valid @RequestBody CreateMember request){
LocalDateTime dateTime = LocalDateTime.now();
return ApiResponse.ok(memberService.CreateMember(request, dateTime));
}
@GetMapping("/member/{id}")
public ApiResponse<MemberResponse> SelectMember(@PathVariable Long id){
return ApiResponse.ok(memberService.SelectMember(id));
}
}
@RequiredArgsConstructor
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberResponse CreateMember(CreateMember request, LocalDateTime dateTime) {
Member entity = request.toEntity(request, dateTime);
Member res = memberRepository.save(entity);
return MemberResponse.of(res);
}
public MemberResponse SelectMember(Long id) {
Optional<Member> member = memberRepository.findById(id);
return MemberResponse.of(member);
}
}
📚Conroller 계층 단위 테스트
- @WebMvcTest(controllers = MemberController.class)
- @WebMvcTest는 Spring MVC 관련 컴포넌트만 로드하고, 컨트롤러 계층만 테스트하는 어노테이션입니다.
- Service, Repository 계층은 실행되지 않습니다. 그래서 data가 null이 될 수 있습니다.
- @Autowired
- 의존성 주입(DI)을 통해 객체를 자동으로 주입받기 위해 사용합니다.
- MockMvc
- MockMvc는 Spring MVC 테스트를 위한 객체로, 실제 HTTP 요청을 보내지 않고도 웹 레이어의 동작을 테스트할 수 있습니다.
- MockMvc를 사용하면 실제 서버를 실행하지 않고도 HTTP 요청을 모의로(mock), 컨트롤러가 제대로 동작하는지 확인할 수 있습니다. 이 객체를 통해 GET, POST, PUT, DELETE 등의 요청을 보내고 그 응답을 검증할 수 있습니다.
- @MockitoBean
- @MockitoBean은 이제 가짜 객체(mock)를 Spring 컨텍스트에 주입하여, 테스트에서 실제 객체 대신 모킹된 객체를 사용할 수 있게 해주는 어노테이션입니다.
- 실제 MemberService 객체 대신 가짜 MemberService 사용되어 컨트롤러에서 의존성 주입을 받게되어 "컨트롤러의 동작만 테스트하고, MemberService의 실제 비즈니스 로직을 수행하지 않습니다."
- ObjectMapper
- ObjectMapper는 객체와 JSON 간의 변환을 처리하는 Jackson 라이브러리의 주요 클래스입니다.
- ObjectMapper는 Java 객체를 JSON으로 직렬화하거나, JSON을 Java 객체로 역직렬화할 때 사용됩니다.
- 예를 들어, CreateMember 객체를 JSON 형식으로 변환해 HTTP 요청에 포함시키거나, 응답을 ObjectMapper를 통해 Java 객체로 변환하여 검사할 수 있습니다.
@WebMvcTest(controllers = MemberController.class)
class MemberControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockitoBean
private MemberService memberService;
}
📜회원 등록 성공시 200 OK 검증 테스트
- given 준비 단계에서 CreateMember객체를 생성
- mockMvc.perform() : MockMvc를 사용하여 HTTP 요청을 실행
- post("/member") : POST 방식으로 /member 엔드포인트를 호출
- objectMapper.writeValueAsString(request) : CreateMember 객체를 JSON 문자열로 변환, 실제 요청 시 JSON 형태로 전달해야 하기 때문
- content() : 변환된 JSON 데이터를 HTTP 요청의 Body에 담음
실제 응답 값을 보면 data에 회원 정보가 잘 들어오지만, 현재 테스트 코드에서는 Mockito를 사용하여 MemberService를 가짜(Mock) 객체로 대체했기 때문에, 실제 DB에 회원 정보가 등록되지 않습니다. 따라서 data 값이 null로 나오는 것입니다.
@DisplayName("신규 회원을 등록한다.")
@Test
void CreateMember() throws Exception {
// given
CreateMember request = CreateMember.builder()
.userName("이름")
.address("주소")
.phone("010-1111-1111")
.build();
// when // then
mockMvc.perform(post("/member")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON) // 요청 타입을 application/json으로 설정
)
.andDo(print()) // 용청과 응답 내용을 콘솔에 출력 (디버깅용)
.andExpect(status().isOk()) // 응답 상태 코드가 200 OK인지 검증
;
}
// 실제 응답 값
{
"status": "OK",
"message": "OK",
"data": {
"id": 1,
"userName": "이름",
"address": "주소",
"phone": "010111111",
"createdAt": "2025-03-21T22:02:52.9993217"
},
"code": 200
}
💩회원 등록시 Vaild 검증 테스트
- jsonPath()는 JSON 응답에서 특정 필드를 찾아 값을 검증하는 역할
- $ → 최상위 JSON 객체를 의미
- $.code → "code" 필드 값을 가져 옴
- jsonPath("$.code").value("400") : "code" 필드 값 과 value값 비교
- jsonPath("$.data").isEmpty() → 응답 JSON의 data 필드가 비어 있는지 검증
이 처럼 테스트 코드는 응답 값이 어떻게 나오는지 알고 있어야 하고, 잘못된 요청에 대한 응답도 예상 결과 값을 미리 알고 있어야 테스트 코드를 작성 할 수 있습니다.
@DisplayName("신규 회원을 등록할 때 회원 이름은 필수입니다.")
@Test
void CreateMemberWithNotUserName() throws Exception {
// given
CreateMember request = CreateMember.builder()
.userName("")
.address("주소")
.phone("010-1111-1111")
.build();
// when // then
mockMvc.perform(post("/member")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().isBadRequest()) // HTTP 상태 코드가 400인지 확인
.andExpect(jsonPath("$.code").value("400"))
.andExpect(jsonPath("$.status").value("BAD_REQUEST"))
.andExpect(jsonPath("$.message").value("회원 이름은 필수 입니다."))
.andExpect(jsonPath("$.data").isEmpty());
}
// 실제 응답 값
{
"status": "BAD_REQUEST",
"message": "회원 이름은 필수 입니다.",
"data": null,
"code": 400
}
🤓PathVariable 을 통한 테스트 작성
앤드포인에 요청을 할 때 PathVariable이나 QueryParam같은 경우는 아래 코드처럼 작성하면 됩니다.
@DisplayName("회원을 조회한다.")
@Test
void SelectMember() throws Exception {
// given
Long memberId = 1L;
// when // then
mockMvc.perform(
get("/member/{id}", memberId)
// .queryParam("name", "이름") Parm은 이렇게 작성
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("200"))
.andExpect(jsonPath("$.status").value("OK"))
.andExpect(jsonPath("$.message").value("OK"))
;
}
🌟Service 계층 단위 테스트
- @SpringBootTest : Spring 컨텍스트를 로드하여 실제 Bean들이 주입됩니다. Mock이랑 다르게 실제로 동작함
- deleteAllInBatch() : 각 테스트가 끝날 때마다 deleteAllInBatch()를 호출하여 테스트 데이터 삭제, 즉 DB에 데이터가 저장되었다가 테스트가 끝나면 롤백 대신 삭제 됨
- extracting() : 객체에서 특정 조회 필드만 선택해서 추출
- containsExactly() : 추출한 결과 값 들이 기대한 값과 순서까지 일치하는지 비교
@ActiveProfiles("test")
@SpringBootTest
class MemberServiceTest {
@Autowired
private MemberService memberService;
@Autowired
private MemberRepository memberRepository;
@AfterEach
void tearDown() {
memberRepository.deleteAllInBatch();
}
@DisplayName("회원 생성 성공")
@Test
void CreateMember() {
// given
LocalDateTime now = LocalDateTime.now();
CreateMember request = new CreateMember("이름", "주소", "010-1111-1111");
// when
MemberResponse response = memberService.CreateMember(request, now);
// then
assertThat(response)
.extracting("userName", "address", "phone", "createdAt")
.containsExactly("이름", "주소", "010-1111-1111", now);
}
}
💀서비스 계층 테스트에서 예외 터트리기
- assertThatThrownBy() : 코드 실행 시 예외가 발생하는지 검증
- isInstanceOf() : 발생한 예외가 IllegalArgumentException 인지 검즘
- hasMessage() : 예외 메시지가 기대한 값과 일치하는지 검
assertThat()는 기대한 값과 일치하는지 검증이고 assertThatThrownBy()는 예외가 일치하는지 검증하는 메서드이다.
@DisplayName("없는 회원Id로 조회한다..")
@Test
void SelectMemberWhitNotId() {
// given
Long id = 100L;
// when // then
assertThatThrownBy(() -> memberService.SelectMember(id))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("없는 회원입니다.");
}
'Spring Boot' 카테고리의 다른 글
[Spring-Boot] Promtail + Loki + Logback 모니터링 (0) | 2025.03.09 |
---|---|
[Spring-Boot] 객체 지향 설계 원칙 SOLID (0) | 2025.02.25 |
[Spring-Boot] QueryDsl 적용해보기 (0) | 2025.02.17 |
[Spring-Boot] Prometheus + Grafana 모니터링 구축 (1) | 2025.02.09 |
[Spring-Boot] 스프링 데이터 JPA (0) | 2025.02.04 |