본문 바로가기
코딩 공부/web & Java

[Java] 테스트 더블(Test Double) (feat. Mock, Stub)

by 현장 2023. 12. 16.

테스트 더블(Test Double)

테스트를 진행하기 어려운 경우 이를 대신해 테스트를 진행할 수 있도록 만들어주는 객체를 말합니다.

 

예를 들어 우리가 데이터베이스로부터 조회한 값을 연산하는 로직을 구현했다면, 해당 로직을 테스트하기 위해선 항상 데이터베이스의 영향을 받을 것입니다. 이는 데이터베이스의 상태에 따라 다른 결과를 유발할 수도 있습니다.

이렇게 테스트하려는 객체와 연관된 객체를 사용하기가 어렵고 모호할 때 대신해 줄 수 있는 객체를 테스트 더블이라 합니다.

🏷️ 테스트 더블의 종류

1. Dummy

// 인터페이스
public interface PringWarning {
    void print();
}

// 인터페이스를 구현하는 클래스
public class PrintWarningDummy implements PrintWarning {
    @Override
    public void print() {
        // 아무런 동작을 하지 않는다.
        // 실제 객체가 로그용 경고만 출력한다면 테스트 환경에서는 전혀 필요 없기 때문이다.
    }
}

// 이런 경우에는 print() 가 아무런 동작을 하지 않아도 테스트에는 영향을 미치지 않습니다.

가장 기본적인 테스트 더블입니다. 인스턴스화된 객체가 필요하지만 기능은 필요하지 않은 경우에 사용하며, Dummy 객체의 메서드가 호출되었을 때 정상 동작은 보장하지 않습니다. 객체는 전달되지만 사용되지 않는 객체입니다.

 

정리하면 인스턴스화된 객체가 필요해서 구현한 가짜 객체일 뿐이고, 생성된 Dummy 객체는 정상적인 동작을 보장하지 않습니다.

2. Fake

// 실제 데이터베이스 대신 가짜 데이터베이스 역할을 하는 
// FakeUserRepository를 만들어 테스트 객체에 주입하는 방법도 있다. 
public class FakeUserRepository implements UserRepository {
    private Collection<User> users = new ArrayList<>();
    
    @Override
    public void save(User user) {
        if (findById(user.getId()) == null) {
            user.add(user);
        }
    }
    
    @Override
    public User findById(long id) {
        for (User user : users) {
            if (user.getId() == id) {
                return user;
            }
        }
        return null;
    }
}

복잡한 로직이나 객체 내부에서 필요로 하는 다른 외부 객체들의 동작을 단순화하여 구현한 객체입니다. 동작의 구현을 가지고 있지만 실제 프로덕션에는 적합하지 않은 객체입니다.

정리하면 동작은 하지만 실제 사용되는 객체처럼 정교하게 동작하지는 않는 객체를 말한다.

3. Stub

public class StubUserRepository implements UserRepository {
    // ...
    @Override
    public User findById(long id) {
        return new User(id, "Test User");
    }
    // 테스트 환경에서 User 인스턴스의 name을 Test User만 받기를 원하는 경우 
    // 이처럼 동작하는 객체(UserRepository의 구현체)를 만들어 사용할 수 있다.
}


Dummy 객체가 실제로 동작하는 것 처럼 보이게 만들어 놓은 객체입니다. 인터페이스 또는 기본 클래스가 최소한으로 구현된 상태며, 테스트에서 호출된 요청에 대해 미리 준비해 둔 결과를 제공합니다.

정리하면 테스트를 위해 프로그래밍된 내용에 대해서만 준비된 결과를 제공하는 객체입니다.

4. Spy

public class MailingService {
    private int sendMailCount = 0;
    private Collection<Mail> mails = new ArrayList<>();

    public void sendMail(Mail mail) {
        // MailingService는 sendMail을 호출할 때마다 보낸 메일을 저장하고 몇 번 보냈는지를 체크
        sendMailCount++;
        mails.add(mail);
    }

    public long getSendMailCount() {
        // endMailCount 변수에 저장된 값인 호출 횟수를 반환
        return sendMailCount;
    }
}

Stub의 역할을 가지면서 호출된 내용에 대해 약간의 정보를 기록합니다. 테스트 더블로 구현된 객체에 자기 자신이 호출 되었을 때 확인이 필요한 부분을 기록하도록 구현하며 실제 객체처럼 동작시킬 수도 있고, 필요한 부분에 대해서는 Stub로 만들어서 동작을 지정할 수도 있습니다.

정리하면 실제 객체로도 사용할 수 있고 Stub 객체로도 활용할 수 있으며 필요한 경우 특정 메서드가 제대로 호출되었는지 여부를 확인할 수 있다.

 

✔️ Mockito 프레임워크의 verify() 메서드가 같은 역할을 합니다.

5. Mock

@ExtendWith(MockitoExtension.class)
class SomeBusinessImplMockTest {

    @Mock
    private DataService dataServiceMock;

    @InjectMocks
    private SomeBusinessImpl businessImpl;

    @Test
    void findTheGreatestFromAllDate_basicScenario() {
        // dataServiceMock.retrieveAllDate()의 반환값을 지정
        when(dataServiceMock.retrieveAllDate()).thenReturn(new int[] {25, 15, 5});
        assertEquals(25, businessImpl.findTheGreatestFromAllDate());
    }
}

호출에 대한 기대를 명세하고 내용에 따라 동작하도록 프로그래밍 된 객체입니다.  Mockito 프레임워크가 대표적인 Mock 프레임워크라 볼 수 있습니다.

🏷️ Stub vs Mock 

✅ Stub 

  • 더미 객체를 생성하고 실제로 동작하는것처럼 보이게 만든 가짜 객체입니다. 
  • 호출된 요청에 대한 응답값을 미리 만들어놓고 전달합니다. 
  • 객체의 최소한의 기능만을 임의로 구현합니다. 

✅ Mock

  • 특정 동작을 수행하는지(= 메서드를 제대로 콜 하는지)에 대한 검증을 합니다.
  • 행위검증을 추구한다는 점에서 다른 테스트 더블들과 구분됩니다. 

직관적으로 요약을 해보자면 Stub=상태 검증, Mock=행위 검증이라고 볼 수 있습니다. 

상태 검증
메서드가 수행될 때 연관되어있는 협력 객체의 '상태'를 검증함으로써 제대로 기능이 동작하고 있는지를 검증하는 것입니다. 
행위 검증
테스트하고자 하는 메서드가 참조하고 있는 협력 객체의 메서드를 제대로 콜 하는지에 대한 '행위'를 검증하는 것입니다.

📖 Reference

Tecoble

두둥탁 개발자

 

'코딩 공부 > web & Java' 카테고리의 다른 글

[Spring] Gradle  (0) 2024.01.07
[Spring] AOP  (1) 2024.01.05
[Java] Mockito  (1) 2023.12.15
[Java] JUnit  (0) 2023.12.12
[Web] JWT  (1) 2023.12.08