spring

[Spring 테스트] JUnit + Mockito로 공통코드 서비스 테스트하기 (feat. Collectors.toMap, @Mock, @InjectMocks 이해하기)

devJK93 2025. 5. 1.

Spring + Mockito 환경에서 @Mock, @InjectMocks, 그리고 Stream API의 Collectors.toMap 사용법을 정리해봤다.

실습 코드를 기반으로 테스트 코드 작성법Stream API 사용 이유를 함께 이해해보자.

 

📑 예제 코드 소개

아래는 공통코드 그룹핑 서비스 테스트 예제:

﹡CmmnCodeServiceTest

package wpo.wpms.common;


import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;


import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.extension.ExtendWith;
import wpo.wpms.common.domain.dto.CmmnCodeDTO;
import wpo.wpms.common.domain.dto.GroupedCodeResponseDTO;
import wpo.wpms.common.mapper.CmmnCodeMapper;
import wpo.wpms.common.service.CmmnCodeService;


@ExtendWith(MockitoExtension.class)
class CmmnCodeServiceTest {


   @Mock
   private CmmnCodeMapper mapper;


   @InjectMocks
   private CmmnCodeService service;


   @Test
   @DisplayName("그룹별 공통코드 묶기")
   void getGroupedCodes() {
       // given
       List<CmmnCodeDTO> mockData = List.of(
               createCmmnCodeDTO("position", "dev", "개발자"),
               createCmmnCodeDTO("position", "design", "디자이너"),
               createCmmnCodeDTO("skill_level", "senior", "고급")
       );


       when(mapper.selectCodesByTypes(List.of("position", "skill_level")))
               .thenReturn(mockData);


       // when
       List<GroupedCodeResponseDTO> result = service.getGroupedCodes(List.of("position", "skill_level"));


       // then
       assertThat(result).hasSize(2);


       Map<String, List<String>> resultMap = result.stream()
               .collect(Collectors.toMap(
                       GroupedCodeResponseDTO::getGroupCode,
                       group -> group.getCodes().stream().map(GroupedCodeResponseDTO.CodeItem::getCode).toList()
               ));


       assertThat(resultMap.get("position")).containsExactlyInAnyOrder("dev", "design");
       assertThat(resultMap.get("skill_level")).containsExactly("senior");
   }


   private CmmnCodeDTO createCmmnCodeDTO(String type, String value, String name) {
       CmmnCodeDTO code = new CmmnCodeDTO();
       code.setCodeTy(type);
       code.setCodeValue(value);
       code.setCodeNm(name);
       return code;
   }
}

 

﹡GroupedCodeResponseDTO

public class GroupedCodeResponseDTO {
    private String groupCode;
    private List<CodeItem> codes;

    public static class CodeItem {
        private String code;
        private String name;
    }
}

1️⃣ Collectors.toMap 사용법 하나하나 뜯어보기

Map<String, List<String>> resultMap = result.stream()
    .collect(Collectors.toMap(
        GroupedCodeResponseDTO::getGroupCode,
        group -> group.getCodes().stream().map(GroupedCodeResponseDTO.CodeItem::getCode).toList()
    ));

 

이 코드는 List<GroupedCodeResponseDTO>Map<String, List<String>>으로 바꾸는 과정이다.

 

🔸 단계별 설명

단계 설명
result.stream() List<GroupedCodeResponseDTO>를 스트림으로 변환.
Collectors.toMap() 스트림을 Map으로 변환하기 위한 Collector를 사용.
GroupedCodeResponseDTO::getGroupCode Map의 key로 사용할 값.
즉, "position", "skill_level" 같은 그룹 코드.
group -> group.getCodes().stream().map(GroupedCodeResponseDTO.CodeItem::getCode).toList() Map의 value에 들어갈 List<String>을 만듭니다. 코드 목록("dev", "design", "senior" 등)을 뽑아낸다.

 

✅ GroupedCodeResponseDTO.CodeItem::getCode란?

 

이건 사실 아래 코드를 간단히 줄여 쓴 것이다.

item -> item.getCode()

 

즉, CodeItem 객체에서 getCode() 메서드를 호출해 반환값을 얻는 람다 표현식.

 

📌 어떻게 작동하냐면:

예를 들어 다음과 같은 리스트가 있다고 하자:

List<CodeItem> codes = List.of(
    new CodeItem("dev"),
    new CodeItem("design"),
    new CodeItem("pm")
);

 

그리고 이 리스트에서 code 값만 뽑고 싶다면:

 

1. 람다 방식

List<String> codeStrings = codes.stream()
    .map(item -> item.getCode())
    .toList();

 

2. 메서드 레퍼런스 방식

List<String> codeStrings = codes.stream()
    .map(GroupedCodeResponseDTO.CodeItem::getCode)
    .toList();

 

동일한 결과가 나오고, 두 번째 방식이 더 간결.

 

💡 왜 이렇게 쓸까?

  • 반복되는 람다 (x -> x.getSomething())를 간결하게 표현하려고
  • 코드를 읽기 쉽게 만들기 위해

🔧 구조로 보면

public class GroupedCodeResponseDTO {
    private String groupCode;
    private List<CodeItem> codes;

    public static class CodeItem {
        private String code;

        public String getCode() {
            return code;
        }
    }
}

 

여기서 CodeItem::getCode는 Function<CodeItem, String> 타입의 함수로 취급된다.

즉, CodeItem을 받아서 그 안의 code 값을 리턴하는 함수.

 

💡 예시 변환 결과

[
  {
    groupCode: "position",
    codes: [
      { code: "dev", name: "개발자" },
      { code: "design", name: "디자이너" }
    ]
  },
  {
    groupCode: "skill_level",
    codes: [
      { code: "senior", name: "고급" }
    ]
  }
]
{
  "position": ["dev", "design"],
  "skill_level": ["senior"]
}

2️⃣ @ExtendWith(MockitoExtension.class) + @Mock + @InjectMocks 이해하기

🔹 @ExtendWith(MockitoExtension.class)

JUnit5에서 Mockito를 쓸 수 있도록 확장해주는 애너테이션.

@Mock, @InjectMocks 등을 인식하게 해준다.

 

🔹 @Mock

@Mock
private CmmnCodeMapper mapper;
  • 실제 Mapper 대신 가짜(mock) 객체를 생성해준다.
  • DB에 접근하지 않고도 when(mapper.xxx).thenReturn(...) 식으로 원하는 값을 직접 지정할 수 있다.

🔹 @InjectMocks

@InjectMocks
private CmmnCodeService service;
  • CmmnCodeService를 생성하면서, @Mock으로 만들어진 객체(mapper)를 주입한다.
  • 즉, 의존성을 자동으로 넣어준다. (수동으로 new 하지 않아도 됨).

3️⃣ collect(Collectors.toMap(...)) vs map()

🔹 map()은 변환

list.stream().map(x -> x.toUpperCase()).toList();
  • 각각의 원소를 하나씩 변환한 결과를 새로운 List로 리턴
  • 1:1 매핑

🔹 collect(Collectors.toMap(...))은 변환 + 집계

stream.collect(Collectors.toMap(keyFunction, valueFunction));
  • 스트림의 각 요소를 key/value 쌍으로 변환해서 Map으로 집계
  • 즉, List → Map
항목 map() collect(Collectors.toMap())
목적 요소 변환 (1:1) Map으로 변환 (집계)
리턴 타입 Stream<R> Map<K, V>
사용 시점 중간 연산 (intermediate operation) 종단 연산 (terminal operation)
예: 문자열 길이 뽑기 map(String::length) toMap(str -> str, str -> str.length())

✅ 마무리 요약

개념 한 줄 요약
@Mock 테스트용 가짜 객체
@InjectMocks 가짜 객체를 자동으로 주입
@ExtendWith(MockitoExtension.class) Mockito 기능 활성화
map() 요소 하나씩 변환
Collectors.toMap() 스트림을 Map으로 바꿈

 

 

 

댓글