티스토리 뷰

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()를 사용하고 싶어서 두 번째 해결 방법을 사용했습니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
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
글 보관함