티스토리 뷰
Jpa 개발 관련 포스팅입니다.
해당 예시 코드 pr 주소입니다.
feat: JpaRepository 파라미터 Mocking 시 NullPointerException 예시 pr by kkoon9 · Pull Request #1 · laboratory-kkoon9/sp
github.com
Mocking
Mocking이란?
Mock 이라는 단어의 사전적 의미는 다음과 같습니다.
- (흉내를 내며) 놀리다
- 무시하다
- 거짓된, 가짜의
위 뜻을 미루어 봤을 때, Mocking 이라는 것은 실제 값으로 테스트를 하기 어려우니 가짜 값을 사용할 수 있게 해주는 것입니다.
그래서 보통 테스트 코드를 작성할 때, mocking을 많이 사용합니다.
Mocking이 필요한 상황
보통 다음과 같은 상황에서 Mocking을 사용합니다.
- 테스트 대상에서 파일 시스템을 사용
- 테스트 대상에서 DB로부터 데이터를 조회하거나 데이터를 추가
- 테스트 대상에서 외부의 HTTP 서버와 통신
이번 포스팅에서는 두 번째 상황으로 인해 Mocking을 사용했습니다.
Mock 관련된 내용은 다른 포스팅에서 자세히 다루겠습니다.
배경
해당 나이를 가진 고객들의 리스트를 가져오는 기능을 개발했습니다.
도메인
먼저, 도메인 Customer 코드입니다.
package com.laboratorykkoon9.springjpa.lab.domain;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Getter
@Entity
@Table(name = "customers")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "login_id")
private String loginId;
@Column(name = "password")
private String password;
@Column(name = "nickname")
private String nickname;
@Column(name = "age")
private Integer age;
@Builder
public Customer(Long id, String loginId, String password, String nickname, Integer age) {
this.id = id;
this.loginId = loginId;
this.password = password;
this.nickname = nickname;
this.age = age;
}
}
NPE 테스트가 목적이므로 최소한의 고객 정보만 담았습니다.
레포지터리
레포지터리 CustomerRepository 코드입니다.
package com.laboratorykkoon9.springjpa.lab.repository;
import com.laboratorykkoon9.springjpa.lab.domain.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface CustomerRepository extends JpaRepository<Customer, Long> {
List<Customer> findByAge(int age);
}
서비스
서비스 CustomerService 코드입니다.
package com.laboratorykkoon9.springjpa.lab.application;
import com.laboratorykkoon9.springjpa.lab.domain.Customer;
import com.laboratorykkoon9.springjpa.lab.repository.CustomerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CustomerService {
private final CustomerRepository customerRepository;
@Transactional
public List<Customer> getCustomersByAge(final Integer age) {
List<Customer> customers = customerRepository.findByAge(age);
return customers;
}
}
문제의 테스트 코드
이제 문제의 테스트 코드를 살펴봅시다.
package com.laboratorykkoon9.springjpa.lab.application;
import com.laboratorykkoon9.springjpa.lab.domain.Customer;
import com.laboratorykkoon9.springjpa.lab.repository.CustomerRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
class CustomerServiceTest {
@Mock
private CustomerRepository customerRepository;
@InjectMocks
private CustomerService customerService;
private static final String LOGIN_ID = "eric@gmail.com";
private static final int givenAge = 1;
@BeforeEach
void setUp() {
customerService = new CustomerService(customerRepository);
}
@Test
@DisplayName("해당 나이를 가진 고객들의 리스트를 리턴한다.")
void getCustomersByAge_test1() {
// given
Customer customer = Customer.builder()
.loginId(LOGIN_ID)
.age(givenAge)
.build();
given(customerRepository.findByAge(any())).willReturn(List.of(customer));
// when
List<Customer> customers = customerService.getCustomersByAge(givenAge);
// then
assertAll(
() -> assertThat(customers.size()).isEqualTo(1),
() -> assertThat(customers.get(0).getLoginId()).isEqualTo(LOGIN_ID),
() -> assertThat(customers.get(0).getAge()).isEqualTo(givenAge)
); }
}
mockito에서 제공하는 given을 사용해서 findByAge 메서드를 모킹했습니다.
하지만, 결과는 NullPointerException이 발생했습니다.
NPE의 원인은 문서에서 찾아볼 수 있었습니다.
any()는 제너릭 메서드였는데, 타입 파라미터로 명시할 수 있는 것은 Reference type밖에 없습니다.
int와 같은 primitive type은 불가능한 셈이죠.
해결
이제 해결 방법을 알아봅시다.
해결 방법 [1]. anyInt() 사용
위 문서에서는 primitive type을 사용하려면 anyInt()를 사용하라고 나와있었습니다.
@ExtendWith(MockitoExtension.class)
class CustomerServiceTest {
@Test
@DisplayName("해당 나이를 가진 고객들의 리스트를 리턴한다.")
void getCustomersByAge_test1() {
// given
Customer customer = Customer.builder()
.loginId(LOGIN_ID)
.age(givenAge)
.build();
given(customerRepository.findByAge(anyInt())).willReturn(List.of(customer));
// when
List<Customer> customers = customerService.getCustomersByAge(givenAge);
// then
assertAll(
() -> assertThat(customers.size()).isEqualTo(1),
() -> assertThat(customers.get(0).getLoginId()).isEqualTo(LOGIN_ID),
() -> assertThat(customers.get(0).getAge()).isEqualTo(givenAge)
); }
}
해결 방법 [2]. 파라미터 Wrapper class 사용
any()를 그대로 사용하고 싶어서 jpa 메서드에서 파라미터를 int가 아닌 Integer로 바꾸어주었습니다.
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByLoginId(String loginId);
List<Customer> findByAge(Integer age);
}
결론
저는 범용성이 넓은 any()를 사용하고 싶어서 두 번째 해결 방법을 사용했습니다.
'개발 노트' 카테고리의 다른 글
쿼리 스트링 만들기 리팩터링 (0) | 2023.02.15 |
---|---|
[스프링+코틀린] Execution failed for task ':test'. > No tests found for given includes (0) | 2023.01.21 |
쿼리 스트링 만들기 (0) | 2023.01.02 |
이 로직은 도메인 영역일까? 애플리케이션 영역일까? (0) | 2022.08.07 |
DTO 엔티티 validation과 List<DTO> (0) | 2022.05.28 |
- Total
- Today
- Yesterday
- 프로그래머스
- 이펙티브 자바
- 디자인패턴
- 코테
- BAEKJOON
- 정규표현식
- 테라폼
- 클린 아키텍처
- Kotlin
- 알고리즘
- JPA
- AWS
- 객체지향
- kotest
- programmers
- Algorithm
- node.js
- Effective Java
- Spring
- C++
- Java
- 디자인 패턴
- MSA
- 클린 코드
- 이팩티브 자바
- Spring Boot
- 백준
- Olympiad
- BOJ
- kkoon9
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |