NullNull

Mock? Spy? 본문

프로그래밍 언어/JAVA

Mock? Spy?

KYBee 2025. 2. 4. 22:45

이 둘은 모두 Mockito 라이브러리를 사용해서 활용할 수 있습니다. 간단하게 어노테이션만 붙여주면서 객체의 동작을 제어할 수 있는데요. 이 두 개념은 비슷하지만 중요한 차이가 존재합니다. 이 글에서는 Mock 과 Spy 의 차이를 비교하고, 어떤 경우에 사용해야 하는지 정리해보겠습니다.

 

 

1. Mock vs Spy: 개념적 차이

우선 둘은 주로 단위 테스트 에서 사용됩니다.

Mock

@Mock은 가짜 객체를 생성하는 것입니다.

이 객체는 모든 메서드를 모킹하여 실제 동작 없이 지정된 반환 값을 제공합니다. 즉 특정 클래스 안에 있는 모든 메서드는 껍데기만 존재할 뿐 아무 로직이 없다고 가정합니다. Mockito.when()으로 mocking 하지 않는다면, 아무 동작을 수행하지 않게 됩니다. 물론 mocking 한 메서드에 대해서는 해당 메서드가 불릴 때, 실제 로직 대신 mocking 하도록 작성한 로직을 수행합니다.

Spy

@Spy는 실제 객체를 감싸는 방식입니다.

Mock 과 비슷하게 모든 메서드를 mocking 할 수 있지만, 아무런 로직도 mocking 하지 않는다면 모든 메서드가 실제 객체의 로직대로 실행됩니다. 물론 변경하고 싶은 일부 메서드를 mocking 한다면, 그 메서드는 mocking 한 로직대로 동작하게 됩니다. @Spy는 단위 테스트에서 실제 로직의 일부만 변경하고 싶은 경우에 유용합니다.

 

 

 

2. Mock과 Spy의 동작 방식 비교

Mock: 모든 메서드 모킹

@Mock은 실제 로직을 실행하지 않고 우리가 설정한 반환 값만 반환합니다. 예를 들어, 아래 예시처럼 PlayerRepository의 createPlayer와 getPlayer 메서드를 모두 mocking 하면, 실제 데이터베이스와의 상호작용 없이 테스트를 진행할 수 있습니다. 이렇게 하면 외부 의존성을 격리하고, 순수한 단위 테스트가 가능합니다.

Spy: 일부 메서드만 모킹

@Spy는 메서드는 실제 객체의 로직을 그대로 실행하면서 일부 메서드만 mocking 할 수 있습니다. 아래 예시는, createPlayer는 mocking 하고, getPlayer는 실제 구현을 통해 데이터를 가져오는 방식입니다. 이렇게 하면 객체의 일부분은 실제 동작을, 나머지는 모킹하여 더 세밀한 제어가 가능합니다.

 

 

 

3. Mock과 Spy의 차이점 비교

 

특성 @Mock (모든 메서드 모킹)  @Spy (일부 메서드만 모킹)
동작 방식 모든 메서드가 모킹되며, 실제 로직이 실행되지 않음 일부 메서드만 모킹되고, 나머지는 실제 로직이 실행됨
테스트 방식 두 메서드 모두 모킹하여 반환값만 설정 createPlayer는 모킹하고, getPlayer는 실제 실행
검증 방식 두 메서드 모두 모킹된 상태에서 검증 실제 동작을 실행한 getPlayer 메서드는 실제 로직대로 동작

 

4. 코드 예시로 보는 Mock과 Spy의 차이

 

아래 2개의 Class 가 존재하고, PlayerService 에 대한 테스트를 작성한다고 가정하겠습니다.

 

PlayerServiceImpl Class

package com.example.soccerteam.player;

import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;

@Service
public class PlayerServiceImpl implements PlayerService {

    private final PlayerRepository playerRepository;

    public PlayerServiceImpl(PlayerRepository playerRepository) {
        this.playerRepository = playerRepository;
    }

    @Override
    public Player createPlayer(Player player) {
        return playerRepository.createPlayer(player);
    }

    @Override
    public Player getPlayer(String name) {
        return playerRepository.getPlayer(name);
    }
}

 

PlayerRepositoryImpl Class

package com.example.soccerteam.player;

import org.springframework.stereotype.Repository;
import java.util.HashMap;
import java.util.Map;

@Repository
public class PlayerRepositoryImpl implements PlayerRepository {

    private final Map<String, Player> playerStorage = new HashMap<>();

    public Player createPlayer(Player player) {
        playerStorage.put(player.getName(), player);
        return player;
    }

    public Player getPlayer(String name) {
        return playerStorage.get(name);
    }
}

 

Mock 사용 예시: 두 개의 메서드를 모두 모킹

class PlayerServiceMockTest {

    @Mock
    private PlayerRepository playerRepository;

    @InjectMocks
    private PlayerService playerService;

    @Test
    void testCreateAndGetPlayerWithMock() {
		    
	//0.
        Player player1 = new Player("HAALAND", "1999.10.18", 9, "Norway", true, 197, 77);
        Player player2 = new Player("DE BRYUNE", "1993.10.18", 17, "Belgium", true, 183, 70);

	//1. 
        when(playerRepository.createPlayer(player1)).thenReturn(player1);
        when(playerRepository.getPlayer("HAALAND")).thenReturn(player1);

        when(playerRepository.createPlayer(player2)).thenReturn(player2);
        when(playerRepository.getPlayer("DE BRYUNE")).thenReturn(player2);

	//2. 
        Player result1 = playerService.createPlayer(player1);
        Player result2 = playerService.createPlayer(player2);

	//3.
        Player result1Get = playerService.getPlayer("HAALAND");
        Player result2Get = playerService.getPlayer("DE BRYUNE");

	//4.
        Assertions.assertEquals(player1, result1);
        Assertions.assertEquals(player2, result2);
        Assertions.assertEquals(player1, result1Get);
        Assertions.assertEquals(player2, result2Get);

	//5. 
        verify(playerRepository).createPlayer(player1);
        verify(playerRepository).createPlayer(player2);
        verify(playerRepository).getPlayer("HAALAND");
        verify(playerRepository).getPlayer("DE BRYUNE");
    }
}
  • 동작:
    1. createPlayer, getPlayer 을 모두 mocking 합니다.
    2. playerService.createPlayer 가 불리면, 내부에 있는 playerRepository.createPlayer 에서 실제 객체를 생성하지 않고, mocking 로직에 따라 0번에서 생성한 player1 또는 player2 를 리턴합니다.
    3. getPlayer 가 불리면, 마찬가지로 mokcing 로직에 따라 입력값에 맞는 0번에서 생성한 인스턴스를 리턴합니다. 결국 4, 5 번 테스트는 성공하게 됩니다.
  • 사용 시기: 외부 시스템과의 의존성을 완전히 격리하고, 두 메서드의 동작을 독립적으로 테스트하고 싶을 때 유용합니다.

 

Spy 사용 예시: 두 개의 메서드 중 하나만 모킹

class PlayerServiceSpyTest {

    @Spy
    private PlayerRepository playerRepository = new PlayerRepositoryImpl();

    @InjectMocks
    private PlayerService playerService;

    @Test
    void testCreateAndGetPlayerWithSpy() {
    
	//0.
        Player player1 = new Player("HAALAND", "1999.10.18", 9, "Norway", true, 197, 77);
        Player player2 = new Player("DE BRYUNE", "1993.10.18", 17, "Belgium", true, 183, 70);
       
	//1.
        doReturn(player1).when(playerRepository).createPlayer(player1);
        doReturn(player2).when(playerRepository).createPlayer(player2);

	//2.
        Player result1 = playerService.createPlayer(player1);
        Player result2 = playerService.createPlayer(player2);

	//3. 
        Player result1Get = playerService.getPlayer("HAALAND");
        Player result2Get = playerService.getPlayer("DE BRYUNE");

	//4.
        Assertions.assertEquals(player1, result1);
        Assertions.assertEquals(player2, result2);

	//5.
        Assertions.assertNotNull(result1Get); 
        Assertions.assertNotNull(result2Get); 
        
    }
}
  • 동작:
    1. createPlayer 을 mocking 합니다. 0번에서 mocking 된 인스턴스가 리턴됩니다.
    2. playerService.createPlayer 가 불리면, 내부에 있는 playerRepository.createPlayer 에서 실제 객체를 생성하지 않고, mocking 로직에 따라 0번에서 생성한 player1 또는 player2 를 리턴합니다.
    3. getPlayer은 mocking 되지 않았으므로, 이 테스트에서는 2가지 경우로 동작합니다.
      • 만약, Repository 의 playerStorage 에 선수들이 미리 저장되어 있다면, 실제 저장된 객체를 이름에 맞게 가져옵니다. 그리하여 4, 5번 테스트는 성공하게 됩니다.
      • 미리 선수들이 저장되지 않았다면, 실제 저장된 객체가 없으므로 선수들을 가져올 수 없습니다. 그리하여 4, 5번 테스트는 실패하게 됩니다.
  • 사용 시기: 실제 객체의 동작을 일부만 사용하고, 다른 동작은 모킹하려 할 때 유용합니다. 예를 들어, createPlayer는 테스트가 필요하지만 getPlayer는 실제 동작을 통해 확인하고 싶을 때 적합합니다.

 


 

5. 결론

 

만약 의존성을 완전히 제거하고, 순수 클래스의 기능을 검증하는 단위 테스트를 작성하고 싶다면, Mock 을 사용하여 내부에 있는 모든 외부 의존성을 mocking 하여 테스트를 수행할 수 있습니다.

반면에, 특정 클래스의 기능을 검증할 때, 너무 많은 메서드를 mocking 해야 하거나, 이미 잘 동작되는 것을 확인하여 mocking 이 필요없는 경우가 존재한다면, Spy 를 사용하여 테스트를 수행할 수 있습니다.

'프로그래밍 언어 > JAVA' 카테고리의 다른 글

ObjectMother 패턴 적용하기  (0) 2024.04.04
간단한 Unit Test 작성하기  (0) 2024.04.04
객체지향 언어의 특징과 설계 원칙  (0) 2022.09.29
예외 처리  (0) 2022.08.14
Abstract class vs Interface  (0) 2022.08.14
Comments