개발놀이터

Mockito 프레임워크를 이용해 스프링 단위테스트 진행하기 본문

Spring/Spring

Mockito 프레임워크를 이용해 스프링 단위테스트 진행하기

마늘냄새폴폴 2023. 6. 2. 16:25

이번 프로젝트에서 단위테스트를 해야할 일이 생겨서 알아보던 중에 Mockito에 대해서 알게 되었습니다. 

 

스프링을 공부한지 만으로 2년이 되어가지만 아직까지 Mockito를 이용해 테스트 케이스를 만드는 법을 모른다는 사실에 반성하게 되었습니다. 

 

많은 개발자 선배님들이 TDD를 외치셨는데 저는 그걸 지키지 못한 것 같아서 조금 아쉽지만... 지금이라도 TDD를 실천하고자 이렇게 포스팅 적어봅니다. 

 

이번엔 Mockito를 어떤 방식으로 사용해야 하는지 그리고 주요 문법은 무엇이 있는지에 대해서 알아보도록 하겠습니다. 

 

Mockito

Mock 이라는 단어는 가짜, 허구의 라는 뜻입니다. 

 

즉 Mock 객체를 만들어 (가짜 객체를 만들어) DB의 접근 없이도 자신의 코드의 논리적인 부분을 체크하는 방법이 바로 Mockito 프레임워크의 핵심이라고 할 수 있습니다. 

 

즉, findById() 이런 메서드로 반환되는 객체를 가짜로 주입해서 코드의 논리적인 부분을 체크하는 방법이죠. 

 

때문에 Mock객체는 모두 자신이 한땀한땀 설정해줘야합니다. 

 

한번 어떤식으로 사용하는지 보시죠. 

 

@ExtendWith(MockitoExtension.class)
public class BucketServiceTest {

    @Mock
    BucketRepository bucketRepository;
    @Mock
    TemporaryOrderRepository temporaryOrderRepository;
    @Mock
    ItemDetailRepository itemDetailRepository;
    @Mock
    CacheRepository cacheRepository;
    @Mock
    ItemRepository itemRepository;
    @Mock
    TemporaryOrderService temporaryOrderService;
    @InjectMocks
    BucketService bucketService;

    @Test
    @DisplayName("장바구니에서 수량을 줄이거나 늘리면 개수가 변해야한다.")
    public void test1() throws Exception {
        //given
        ChangeCountDto dto = new ChangeCountDto();
        dto.setBucketId("1");
        dto.setCount("2");

        Member member = Data.createMember();
        Item item = Data.createItem(member);
        Bucket bucket = Data.createBucket(member, null, item);
        TemporaryOrder tOrder = Data.createTOrder(bucket);
        ItemDetail itemDetail = Data.createItemDetail(item);


        when(bucketRepository.findById(Long.parseLong(dto.getBucketId()))).thenReturn(Optional.of(bucket));
        when(temporaryOrderRepository.findTemporaryOrderByBucketId(bucket.getId())).thenReturn(tOrder);
        when(itemDetailRepository.findByItemId(bucket.getItem().getId())).thenReturn(Collections.singletonList(itemDetail));
        //when
        Map<String, String> map = bucketService.isBucketStockZero(dto);
        //then
        Assertions.assertNotNull(map);
        Assertions.assertEquals(map.get("message"), "재고가 남아있지 않습니다. 남은 수량 : 1");
        Assertions.assertEquals(map.get("count"), "2");
    }
 }

 

@Service
@RequiredArgsConstructor
@Transactional
public class BucketService {

    private final BucketRepository bucketRepository;
    private final TemporaryOrderRepository temporaryOrderRepository;
    private final ItemDetailRepository itemDetailRepository;
    private final CacheRepository cacheRepository;
    private final ItemRepository itemRepository;
    private final TemporaryOrderService temporaryOrderService;

    public Map<String, String> isBucketStockZero(ChangeCountDto dto) {
        Map<String, String> map = new HashMap<>();
        int nowStock = 0;
        Bucket findBucket = bucketRepository.findById(Long.parseLong(dto.getBucketId())).orElseThrow(
                () -> new RuntimeException("Not Found Bucket")
        );

        TemporaryOrder tOrder = temporaryOrderRepository.findTemporaryOrderByBucketId(findBucket.getId());

        List<ItemDetail> findItemDetails = itemDetailRepository.findByItemId(findBucket.getItem().getId());

        for (ItemDetail itemDetail : findItemDetails) {
            nowStock = itemDetail.getStock() - Integer.parseInt(dto.getCount());

            if (nowStock <= -1) {
                map.put("message", "재고가 남아있지 않습니다. 남은 수량 : " + itemDetail.getStock());
            }
        }
        TemporaryOrder temporaryOrder = tOrder.changeCount(Integer.parseInt(dto.getCount()));
        map.put("count", String.valueOf(temporaryOrder.getCount()));

        return map;
    }
 }

 

위는 테스트코드고 아래는 제가 테스트할 Service 계층의 코드입니다. 

 

먼저 하나씩 살펴보죠. test 코드부터 보죠. 

 

@ExtendWith(MockitoExtension.class)
public class BucketServiceTest {
}

Mockito 프레임워크를 사용하기 위해서는 먼저 테스트 클래스레벨에 @ExtendWith 어노테이션을 달아줘야합니다. 이게 시작이라고 볼 수 있죠. 

 

    @Mock
    BucketRepository bucketRepository;
    @Mock
    TemporaryOrderRepository temporaryOrderRepository;
    @Mock
    ItemDetailRepository itemDetailRepository;
    @Mock
    CacheRepository cacheRepository;
    @Mock
    ItemRepository itemRepository;
    @Mock
    TemporaryOrderService temporaryOrderService;
    @InjectMocks
    BucketService bucketService;

이것들은 제가 테스트케이스에서 주입한 객체들입니다. 보통 우리는 @Autowired를 이용해 주입하는 것이 익숙합니다만, Mockito에선 가짜 객체를 주입해줘야 합니다. 

 

@Mock에 해당하는 것들은 내가 테스트할 클래스의 의존관계와 같습니다. 

 

즉, @Mock 객체 = Service 클래스에서 DI 받은 것

 

이렇게 생각하시면 됩니다. 

 

그리고 이렇게 가짜 객체를 만들면 @InjectMocks 어노테이션을 이용해 내가 테스트하고 싶은 클래스에 적으면 이 어노테이션이 적힌 클래스에 Mock 객체가 주입됩니다. 

 

그리고 주요 문법 확인하시죠!

 

주요 문법

when(bucketRepository.findById(Long.parseLong(dto.getBucketId()))).thenReturn(Optional.of(bucket));



Bucket findBucket = bucketRepository.findById(Long.parseLong(dto.getBucketId())).orElseThrow(
                () -> new RuntimeException("Not Found Bucket")
        );

제가 코드를 따로 가져와 봤는데요. 제가 잘 설명할 수 있을지 모르겠습니다. 한번 도전해보겠습니다. 

 

우리가 일반적으로 스프링 컨테이너를 띄우면 일어나는 일을 상상해봅시다. 

 

스프링 컨테이너가 빈으로 등록된 Repository를 싱글톤으로 생성하고 Service로직에 가서 DI로 주입을 해줬습니다. 그럼 bucketRepository.findById() 메서드가 데이터베이스를 타고 객체를 가져오겠죠. 

 

하지만 말씀드렸다시피 Mock 객체는 가짜 객체입니다. BucketRepository를 가짜로 주입했으니 스프링 입장에서 findById()에 대한 객체도 뭘 가져와야할지 모르는 상황입니다. 

 

그래서 when(), thenReturn() 메서드를 이용해 값을 세팅해주는 것입니다. 

 

때문에 위의 코드를 풀어 해석하면 다음과 같습니다. 

 

bucketRepository.findById() 를 실행했을 때는 => Optional객체의 bucket이 나온단다. 

 

그럼 스프링은 가짜로 주입했어도 대충 아 findById에는 bucket이 들어가는구나 하고 어물쩡 넘어갑니다. 

 

그렇게 해서 DB와 연결되는 부분들을 모두 Mock 객체로 세팅해주면? 

 

내가 원하는 bucketService.isBucketStockZero() 를 호출했을 때 값이 잘 바인딩 됩니다. 

 

잘 설명이 됐나 모르겠네요...

 

이제 자주 쓰이는 문법에 대해서 알아보죠!

 

*findBy~() 를 호출했을 때 단일 객체가 있어야 하는 경우
when(cacheRepository.findmemberAtCache(loginId)).thenReturn(member);

*Repository 의 save() 메서드 Mock 객체로 만드는 법
when(commentRepository.save(any(Comment.class))).thenAnswer(invocation -> invocation.getArgument(0));
이렇게 하면 save() 메서드를 타고 나온 첫번째 객체를 Mock 객체로 바인딩 해준다. 

*findBy~() 를 호출했을 때 빈 List가 반환되는 경우
when(memberWhoGetCouponRepository.findCouponByMemberId(member.getId())).thenReturn(Collections.emptyList());

*findBy~() 를 호출했을 때 값이 있는 List가 반환되는 경우
when(memberWhoGetCouponRepository.findCouponByMemberId(member.getId())).thenReturn(Collections.singletonList(memberWhoGetCoupon));

 

저도 아직 잘 모르는 초보라... 제가 자주 썼던 문법만 간단하게 추려봤습니다. 

 

 

마치며

여기까지 Mockito를 이용한 단위 테스트에 대해서 알아봤습니다. 

 

기존 통합 테스트만 돌려볼 줄 알았던 제가 조금 부끄러워질 정도로 테스트 성능이 매우 잘나와서 부끄러우면서도 아주 만족스럽습니다. 앞으로 많이 애용하게 될 것 같네요. 

 

긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~

 

'Spring > Spring' 카테고리의 다른 글

Spring Actuator  (1) 2023.07.18
프록시패턴, 데코레이터 패턴  (0) 2023.06.27
스프링 부트 3.0 마이그레이션 가이드  (0) 2023.06.02
스프링에서 동시성 문제 해결하기  (0) 2023.05.21
@Async 어노테이션  (1) 2023.05.16