Test Double

@Wonyuยท March 07, 2025 ยท 6 min read

๐Ÿ“Œ Test Double์ด๋ž€?

โœ… ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ์‹ค์ œ ๊ฐ์ฒด๋ฅผ ๋Œ€์ฒดํ•˜๋Š” ๊ฐ€์งœ ๊ฐ์ฒด
โœ… ๋…๋ฆฝ์ ์ธ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•จ

ํ•„์š”์„ฑ

๐Ÿ”น ๋…๋ฆฝ์„ฑ ๋ณด์žฅ
์‹ค์ œ ๊ฐ์ฒด๊ฐ€ ์™ธ๋ถ€ DB, ๋„คํŠธ์›Œํฌ, ํŒŒ์ผ ์‹œ์Šคํ…œ ๋“ฑ ์™ธ๋ถ€ ์‹œ์Šคํ…œ์— ์˜์กด์„ฑ์„ ๊ฐ€์ง€๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์˜ํ–ฅ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.
Test Double์„ ์‚ฌ์šฉํ•˜๋ฉด ์™ธ๋ถ€ ์š”์†Œ์™€ ๋ถ„๋ฆฌํ•ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ”น ์†๋„ ํ–ฅ์ƒ
์™ธ๋ถ€ ์‹œ์Šคํ…œ ํ˜ธ์ถœ์ด ํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ๋Š” ์‹คํ–‰ ์†๋„๊ฐ€ ๋А๋ฆฌ๋‹ค.
Test Double์„ ์‚ฌ์šฉํ•˜๋ฉด ๋น ๋ฅด๊ฒŒ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ€์งœ ๊ฐ์ฒด๋ฅผ ํ™œ์šฉํ•ด ๋น ๋ฅด๊ฒŒ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ”น ํŠน์ • ์ƒํ™ฉ ์‰ฝ๊ฒŒ ์žฌํ˜„
๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜, ์‘๋‹ต ์ง€์—ฐ ๋“ฑ์˜ ์ƒํ™ฉ์„ ์‰ฝ๊ฒŒ ์žฌํ˜„ํ•˜์—ฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•  ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ“Œ Test Double์˜ ์ข…๋ฅ˜

Test Double์€ ํฌ๊ฒŒ ๋‹ค์„ฏ ๊ฐ€์ง€ ์œ ํ˜•์œผ๋กœ ๋ถ„๋ฅ˜๋œ๋‹ค.
test double

Dummy

โœ… ๋‹จ์ˆœํžˆ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฑ„์šฐ๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
โœ… ํ•„์ˆ˜์ ์œผ๋กœ ์š”๊ตฌ๋˜์ง€๋งŒ, ์‹ค์ œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฐ์ฒด

์˜ˆ์‹œ ์ฝ”๋“œ

class DummyUser implements User {
    @Override
    public String getUsername() {
        return null; // ์•„๋ฌด ๋™์ž‘๋„ ํ•˜์ง€ ์•Š์Œ
    }
}

// ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
@Test
void testDummy() {
    NotificationService notificationService = new NotificationService();
    notificationService.sendNotification(new DummyUser()); // ์‹ค์ œ ์‚ฌ์šฉ X
}

Stub

โœ… ๋ฏธ๋ฆฌ ์ •ํ•ด์ง„ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ์ฒด
โœ… ๋ณต์žกํ•œ ๋กœ์ง ์—†์ด ํ…Œ์ŠคํŠธ์— ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ ๋™์ž‘๋งŒ ์ˆ˜ํ–‰

์˜ˆ์‹œ ์ฝ”๋“œ

class PaymentServiceStub implements PaymentService {
    @Override
    public boolean processPayment(double amount) {
        return true; // ํ•ญ์ƒ ์„ฑ๊ณต ๋ฐ˜ํ™˜
    }
}

// ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
@Test
void testStub() {
    PaymentServiceStub paymentStub = new PaymentServiceStub();
    OrderService orderService = new OrderService(paymentStub);

    assertTrue(orderService.placeOrder(100.0)); // ํ•ญ์ƒ ์„ฑ๊ณต
}

Fake

โœ… ์‹ค์ œ ๊ฐ์ฒด์™€ ๋น„์Šทํ•˜์ง€๋งŒ, ๊ฐ„๋‹จํ•œ ์ธ๋ฉ”๋ชจ๋ฆฌ ์ €์žฅ์†Œ ๋“ฑ์„ ํ™œ์šฉํ•ด ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰
โœ… ์‹ค์ œ ๊ตฌํ˜„์— ๊ฐ€๊น์ง€๋งŒ, ์™„์ „ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์ง€ ์•Š์Œ

์˜ˆ์‹œ ์ฝ”๋“œ

class FakeUserRepository implements UserRepository {
    private Map<String, User> database = new HashMap<>();

    @Override
    public void save(User user) {
        database.put(user.getUsername(), user);
    }

    @Override
    public User findByUsername(String username) {
        return database.get(username);
    }
}

// ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
@Test
void testFake() {
    FakeUserRepository fakeRepo = new FakeUserRepository();
    User user = new User("testUser");
    fakeRepo.save(user);

    assertEquals(user, fakeRepo.findByUsername("testUser")); // ์ €์žฅ๋œ ๊ฐ์ฒด ๋ฐ˜ํ™˜
}

Spy

โœ… ์‹ค์ œ ๊ฐ์ฒด๋ฅผ ๊ฐ์‹ธ ํ˜ธ์ถœ ์ •๋ณด๋ฅผ ๊ฐ์‹œํ•˜๋Š” ๊ฐ์ฒด
โœ… ์‹ค์ œ ๊ฐ์ฒด์˜ ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด์„œ ํ˜ธ์ถœ ์—ฌ๋ถ€, ํšŸ์ˆ˜ ๋“ฑ์˜ ์ •๋ณด ๊ฒ€์ฆ ๊ฐ€๋Šฅ

์˜ˆ์‹œ ์ฝ”๋“œ

class SpyList<T> extends ArrayList<T> {
    private int addCallCount = 0;

    @Override
    public boolean add(T element) {
        addCallCount++;
        return super.add(element);
    }

    public int getAddCallCount() {
        return addCallCount;
    }
}

// ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
@Test
void testSpy() {
    SpyList<String> spyList = new SpyList<>();

    spyList.add("hello");
    spyList.add("world");

    assertEquals(2, spyList.getAddCallCount()); // add()๊ฐ€ ๋ช‡ ๋ฒˆ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆ
}

๐Ÿ”น Mock๊ณผ์˜ ์ฐจ์ด์ 
Mock์€ ๋ชจ๋“  ๋™์ž‘์„ ๊ฐ€๋กœ์ฑ„์ง€๋งŒ, Spy๋Š” ์‹ค์ œ ๋ฉ”์„œ๋“œ ์‹คํ–‰ํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๋ฅผ ์ฃผ์˜ํ•ด์•ผ ํ•œ๋‹ค.

Mock

โœ… ํ˜ธ์ถœ์— ๋Œ€ํ•œ ๊ธฐ๋Œ€๋ฅผ ๋ช…์„ธํ•˜๊ณ , ํ•ด๋‹น ๋‚ด์šฉ์— ๋”ฐ๋ผ ๋™์ž‘ํ•˜๋„๋ก ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋œ ๊ฐ์ฒด

์˜ˆ์‹œ ์ฝ”๋“œ

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    public void shouldReturnUserWhenUserIdIsProvided() {
        // Given
        Long userId = 1L;
        User mockUser = new User(userId, "John Doe", "john@example.com");
        when(userRepository.findById(userId)).thenReturn(mockUser);

        // When
        User result = userService.getUserById(userId);

        // Then
        assertNotNull(result);
        assertEquals("John Doe", result.getName());
        verify(userRepository).findById(userId);
    }
    
    @Test
    public void shouldThrowExceptionWhenUserNotFound() {
        // Given
        Long userId = 2L;
        when(userRepository.findById(userId)).thenThrow(new UserNotFoundException("User not found"));

        // When & Then
        assertThrows(UserNotFoundException.class, () -> userService.getUserById(userId));
        verify(userRepository).findById(userId);
    }
}

๐Ÿ“Œ ํ–‰์œ„ ๊ฒ€์ฆ & ์ƒํƒœ ๊ฒ€์ฆ

ํ–‰์œ„ ๊ฒ€์ฆ

ํ…Œ์ŠคํŠธ ๋Œ€์ƒ(SUT)์ด ์˜ˆ์ƒ๋Œ€๋กœ ๋‹ค๋ฅธ ๊ฐ์ฒด์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š”์ง€(ํŠน์ • ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜๋Š”์ง€) ํ™•์ธ

์ƒํƒœ ๊ฒ€์ฆ

ํŠน์ • ์ž‘์—… ์ˆ˜ํ–‰ ํ›„ ๋ฐ˜ํ™˜๊ฐ’์ด ์˜ˆ์ƒ๋Œ€๋กœ ๋‚˜์˜ค๋Š”์ง€ ํ™•์ธ

๋น„๊ต

when(userRepository.findById(userId)).thenReturn(mockUser);

Mock ๊ฐ์ฒด์ธ userRepository๋ฅผ Stub์ฒ˜๋Ÿผ ํ–‰๋™ํ•˜๊ฒŒ ํ•œ๋‹ค.
ํ•˜์ง€๋งŒ, "์‚ฌ์ „ ์ •์˜๋œ ๊ธฐ๋Œ€"๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ๋‹ค๋Š” ์ ์—์„œ Mock ๊ฐ์ฒด์ด๋‹ค.

assertNotNull(result);
assertEquals("John Doe", result.getName());

์ƒํƒœ ๊ฒ€์ฆ์ด์ง€๋งŒ, ๊ทธ ์ƒํƒœ๊ฐ€ ํŠน์ • ํ–‰์œ„(UserRepository์˜ ํ˜ธ์ถœ)์— ์˜ํ•ด ๋งŒ๋“ค์–ด์ง„ ๊ฒƒ์ด๋ฏ€๋กœ, ๊ฐ„์ ‘์ ์œผ๋กœ ํ–‰์œ„ ๊ฒ€์ฆ์˜ ์˜๋ฏธ๋„ ํฌํ•จ๋  ์ˆ˜ ์žˆ๋‹ค.

verify(userRepository).findById(userId);

์‹ค์ œ ์ƒํ˜ธ ์ž‘์šฉ์ด ์ˆ˜ํ–‰๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ–‰์œ„ ๊ฒ€์ฆ์ด๋‹ค.

๐Ÿ”น Mock vs Stub

  • Stub์€ ๋‹จ์ˆœํžˆ ์ž…๋ ฅ๊ฐ’์— ๋Œ€ํ•ด ๊ณ ์ •๋œ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. (์ƒํƒœ ๊ฒ€์ฆ)
  • Mock์€ ๋ช…์„ธ๋Œ€๋กœ ํ–‰๋™ํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ๋ฐ˜ํ™˜๊ฐ’ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ƒํ˜ธ ์ž‘์šฉ์ด ์˜ˆ์ƒ๋Œ€๋กœ ์ˆ˜ํ–‰๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค. (์ƒํƒœ ๊ฒ€์ฆ, ํ–‰์œ„ ๊ฒ€์ฆ)

๐Ÿ“Œ ์ฐธ๊ณ 

https://stackoverflow.com/questions/3459287/whats-the-difference-between-a-mock-stub

@Wonyu
Hello :) I'm Wonyu