Mockito 의 Spy

JavaBeginner
지금 연습하기

소개

Mockito 는 Java 애플리케이션의 모킹 (mocking) 및 **단위 테스트 (unit testing)**에 사용되는 강력한 Java 프레임워크입니다. 이 랩에서는 Mockito 의 스파이 (spy) 개념을 탐구합니다. 스파이는 부분적인 모의 객체 (partial mock) 역할을 하는 독특한 테스트 도구로, 실제 메서드가 호출되도록 허용하면서 객체와의 상호 작용을 추적합니다.

이중적인 동작 덕분에 스파이는 객체와의 상호 작용을 확인하고 싶지만 실제 기능을 실행해야 할 때 특히 유용합니다. 이 랩을 통해 다양한 접근 방식을 사용하여 스파이를 생성하는 방법, 스파이의 동작을 스텁 (stub) 하는 방법, 그리고 스파이와 모의 객체 (mock) 를 비교하여 주요 차이점을 강조하는 방법을 배우게 됩니다.

이것은 가이드 실험입니다. 학습과 실습을 돕기 위한 단계별 지침을 제공합니다.각 단계를 완료하고 실무 경험을 쌓기 위해 지침을 주의 깊게 따르세요. 과거 데이터에 따르면, 이것은 중급 레벨의 실험이며 완료율은 68%입니다.학습자들로부터 97%의 긍정적인 리뷰율을 받았습니다.

Java 프로젝트 생성 및 Mockito Spy 이해

이 단계에서는 프로젝트 구조를 설정하고 Mockito 에서 스파이 (Spy) 가 무엇인지 이해합니다. 스파이를 사용하면 실제 구현을 사용하면서 실제 객체에 대한 메서드 호출을 추적할 수 있습니다.

Java 파일 생성

먼저 이 랩을 위한 메인 Java 파일을 생성해 보겠습니다. WebIDE 에서 ~/project 디렉토리에 MockitoSpyDemo.java라는 새 파일을 생성합니다.

새 파일 생성

필요한 종속성을 가져오기 위해 다음 코드를 추가합니다.

import org.junit.Test;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.MockitoAnnotations;
import org.mockito.exceptions.misusing.NotAMockException;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;

public class MockitoSpyDemo {

    public static void main(String[] args) {
        System.out.println("This is a Mockito Spy demonstration.");
        System.out.println("To run the tests, you need to use JUnit.");
    }
}

Mockito Spy 이해

Mockito 의 스파이는 실제 객체를 감싸는 래퍼 (wrapper) 로, 다음을 수행할 수 있습니다.

  1. 객체와의 모든 상호 작용을 추적합니다 (모의 객체와 유사).
  2. 객체의 실제 메서드를 실행합니다 (모의 객체와 다름).

스파이를 객체에서 일어나는 일을 관찰하지만, 특별히 프로그래밍되지 않는 한 정상적인 동작을 방해하지 않는 관찰자 (observer) 로 생각할 수 있습니다.

스파이의 기본 사용법을 보여주기 위해 첫 번째 테스트 메서드를 추가해 보겠습니다. 다음 메서드를 MockitoSpyDemo 클래스에 추가합니다.

@Test
public void basicSpyExample() {
    // Create a list and spy on it
    List<String> realList = new ArrayList<>();
    List<String> spyList = Mockito.spy(realList);

    // Using the spy to add an item (the real method is called)
    spyList.add("Hello");

    // Verify the interaction happened
    Mockito.verify(spyList).add("Hello");

    // The real method was called, so the list actually has the item
    assertEquals(1, spyList.size());
    assertEquals("Hello", spyList.get(0));

    System.out.println("Basic Spy Example - Spy List Content: " + spyList);
    System.out.println("Basic Spy Example - Spy List Size: " + spyList.size());
}

코드를 컴파일하고 실행하여 작동하는지 확인해 보겠습니다.

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java
java -cp .:lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo
코드 실행

다음 출력을 볼 수 있습니다.

This is a Mockito Spy demonstration.
To run the tests, you need to use JUnit.

메인 메서드가 올바르게 실행됩니다. 그러나 테스트를 실행하려면 JUnit 을 사용해야 합니다. 이 랩을 위해 복잡한 JUnit 러너 (runner) 를 설정하는 대신, 코드 구조와 개념을 이해하는 데 집중할 것입니다.

다음 단계에서는 Mockito 에서 스파이를 생성하는 다양한 방법을 탐구할 것입니다.

Mockito 에서 Spy 를 생성하는 다양한 방법

이 단계에서는 Mockito 에서 스파이를 생성하는 다양한 접근 방식을 탐구합니다. Mockito.spy() 메서드와 @Spy 어노테이션을 사용하여 스파이를 생성하는 방법을 배우게 됩니다.

Mockito.spy() 메서드 사용

스파이를 생성하는 가장 직접적인 방법은 Mockito.spy() 메서드를 사용하는 것입니다. 이 접근 방식을 보여주는 테스트 메서드를 추가해 보겠습니다.

@Test
public void testSpyWithMockitoSpyMethod() {
    // Create an ArrayList and a spy of it
    ArrayList<Integer> realList = new ArrayList<>();
    ArrayList<Integer> spyList = Mockito.spy(realList);

    // Adding elements to the spy (real methods are called)
    spyList.add(5);
    spyList.add(10);
    spyList.add(15);

    // Verifying interactions
    Mockito.verify(spyList).add(5);
    Mockito.verify(spyList).add(10);
    Mockito.verify(spyList).add(15);

    // Verifying that elements were actually added to the list
    assertEquals(3, spyList.size());

    System.out.println("Spy Method Example - Spy List Content: " + spyList);
    System.out.println("Spy Method Example - Spy List Size: " + spyList.size());
}

@Spy 어노테이션 사용

스파이를 생성하는 또 다른 방법은 @Spy 어노테이션을 사용하는 것입니다. 이 접근 방식은 스파이를 자동으로 초기화하려는 경우 특히 유용합니다. 다음 코드를 MockitoSpyDemo 클래스에 추가합니다.

// Declare a spy using annotation
@Spy
ArrayList<Integer> annotationSpyList = new ArrayList<>();

@Before
public void initSpies() {
    // Initialize mocks and spies with annotations
    MockitoAnnotations.initMocks(this);
}

@Test
public void testSpyWithAnnotation() {
    // Adding elements to the spy
    annotationSpyList.add(5);
    annotationSpyList.add(10);
    annotationSpyList.add(15);

    // Verifying interactions
    Mockito.verify(annotationSpyList).add(5);
    Mockito.verify(annotationSpyList).add(10);
    Mockito.verify(annotationSpyList).add(15);

    // Verifying that elements were actually added to the list
    assertEquals(3, annotationSpyList.size());

    System.out.println("Annotation Spy Example - Spy List Content: " + annotationSpyList);
    System.out.println("Annotation Spy Example - Spy List Size: " + annotationSpyList.size());
}

접근 방식 간의 차이점 이해

두 접근 방식 모두 스파이를 생성하지만, 서로 다른 사용 사례가 있습니다.

  1. Mockito.spy()는 더 유연하며 테스트 메서드 어디에서나 사용할 수 있습니다.
  2. @Spy 어노테이션은 스파이가 많고 한 번에 모두 초기화하려는 경우 더 편리합니다.

MockitoAnnotations.initMocks(this) 메서드는 모든 어노테이션된 필드를 초기화하는 역할을 합니다. 실제 프로젝트에서는 @RunWith(MockitoJUnitRunner.class)를 사용하여 이 초기화를 자동으로 수행할 수 있습니다.

업데이트된 코드를 컴파일해 보겠습니다.

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

코드는 오류 없이 컴파일되어야 합니다. 테스트 메서드는 이제 준비되었지만, 앞서 언급했듯이 자동으로 실행하려면 JUnit 러너가 필요합니다.

다음 단계에서는 스파이의 기본 동작을 재정의하기 위해 스텁 (stub) 하는 방법을 배우게 됩니다.

Spy Stubbing 및 예외 처리

이 단계에서는 스파이의 기본 동작을 재정의하기 위해 스텁하는 방법과 Mockito 를 사용할 때 발생할 수 있는 예외를 처리하는 방법을 배우게 됩니다.

스파이 스텁하기

스파이는 기본적으로 실제 객체 메서드를 사용하지만, 때로는 테스트 목적으로 이 동작을 변경하고 싶을 수 있습니다. 이를 스파이 "스텁 (stub)"이라고 합니다. 스파이를 스텁하는 방법을 보여주는 테스트 메서드를 추가해 보겠습니다.

@Test
public void testStubbingSpy() {
    // Create a spy of ArrayList
    ArrayList<Integer> spyList = Mockito.spy(new ArrayList<>());

    // Add an element
    spyList.add(5);

    // Verify the element was added
    Mockito.verify(spyList).add(5);

    // By default, contains() should return true for element 5
    assertTrue(spyList.contains(5));
    System.out.println("Stubbing Example - Default behavior: spyList.contains(5) = " + spyList.contains(5));

    // Stub the contains() method to always return false for the value 5
    Mockito.doReturn(false).when(spyList).contains(5);

    // Now contains() should return false, even though the element is in the list
    assertFalse(spyList.contains(5));
    System.out.println("Stubbing Example - After stubbing: spyList.contains(5) = " + spyList.contains(5));

    // The element is still in the list (stubbing didn't change the list content)
    assertEquals(1, spyList.size());
    assertEquals(Integer.valueOf(5), spyList.get(0));
    System.out.println("Stubbing Example - Spy List Content: " + spyList);
}

스텁에 대한 중요 사항

스파이 메서드를 스텁할 때는 when() 대신 doReturn(), doThrow(), doAnswer() 등을 사용하는 것이 좋습니다. 이는 스파이를 사용할 때 when().thenReturn() 구문이 스텁하는 동안 실제 메서드를 호출할 수 있으며, 이로 인해 예상치 못한 부작용이 발생할 수 있기 때문입니다.

예를 들어:

// This might call the real get() method, causing issues if index 10 doesn't exist
Mockito.when(spyList.get(10)).thenReturn(99); // May throw IndexOutOfBoundsException

// This is the correct way to stub a spy
Mockito.doReturn(99).when(spyList).get(10); // Safe, doesn't call the real method

NotAMockException 처리

일반 객체 (모의 객체 또는 스파이가 아님) 에 Mockito 의 검증 메서드를 사용하려고 하면 NotAMockException이 발생합니다. 이 예외를 보여주는 테스트 메서드를 추가해 보겠습니다.

@Test
public void testNotAMockException() {
    try {
        // Create a regular ArrayList (not a mock or spy)
        ArrayList<Integer> regularList = new ArrayList<>();
        regularList.add(5);

        // Try to verify an interaction on a regular object
        Mockito.verify(regularList).add(5);

        fail("Expected NotAMockException was not thrown");
    } catch (NotAMockException e) {
        // Expected exception
        System.out.println("NotAMockException Example - Caught expected exception: " + e.getMessage());
        assertTrue(e.getMessage().contains("Argument passed to verify() is not a mock"));
    }
}

업데이트된 코드를 컴파일해 보겠습니다.

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

코드는 오류 없이 컴파일되어야 합니다. 이 단계에서는 스파이 메서드를 스텁하여 기본 동작을 재정의하는 방법과 일반 객체에서 검증 메서드를 사용할 때 발생하는 NotAMockException을 처리하는 방법을 배웠습니다.

다음 단계에서는 모의 객체와 스파이를 비교하여 주요 차이점을 이해할 것입니다.

Mock vs Spy: 차이점 이해하기

이 단계에서는 모의 객체 (mock) 와 스파이 (spy) 를 직접 비교하여 주요 차이점을 이해합니다. 이 비교는 테스트 요구 사항에 맞는 올바른 도구를 선택하는 데 도움이 됩니다.

비교 테스트 생성

모의 객체와 스파이의 차이점을 보여주는 테스트 메서드를 추가해 보겠습니다.

@Test
public void testMockVsSpyDifference() {
    // Create a mock and a spy of ArrayList
    ArrayList<Integer> mockList = Mockito.mock(ArrayList.class);
    ArrayList<Integer> spyList = Mockito.spy(new ArrayList<>());

    // Add elements to both
    mockList.add(1);
    spyList.add(1);

    // Verify interactions (both will pass)
    Mockito.verify(mockList).add(1);
    Mockito.verify(spyList).add(1);

    // Check the size of both lists
    System.out.println("Mock vs Spy - Mock List Size: " + mockList.size());
    System.out.println("Mock vs Spy - Spy List Size: " + spyList.size());

    // Mock returns default values (0 for size) unless stubbed
    assertEquals(0, mockList.size());

    // Spy uses real method implementation, so size is 1
    assertEquals(1, spyList.size());

    // Stub both to return size 100
    Mockito.when(mockList.size()).thenReturn(100);
    Mockito.when(spyList.size()).thenReturn(100);

    // Both should now return 100 for size
    assertEquals(100, mockList.size());
    assertEquals(100, spyList.size());

    System.out.println("Mock vs Spy (After Stubbing) - Mock List Size: " + mockList.size());
    System.out.println("Mock vs Spy (After Stubbing) - Spy List Size: " + spyList.size());
}

주요 차이점 이해

Mockito 에서 모의 객체와 스파이의 주요 차이점을 요약해 보겠습니다.

  1. 메서드 동작:

    • Mock: 스텁 (stub) 하지 않는 한 모든 메서드는 기본값 (null, 0, false) 을 반환합니다.
    • Spy: 스텁하지 않는 한 메서드는 실제 구현을 호출합니다.
  2. 객체 상태:

    • Mock: 연산은 상태를 변경하지 않습니다 (예: 모의 목록에 추가해도 실제로 아무것도 추가되지 않음).
    • Spy: 연산은 실제 객체와 마찬가지로 객체의 상태를 변경합니다.
  3. 생성:

    • Mock: 인스턴스가 아닌 클래스에서 생성됩니다 (Mockito.mock(ArrayList.class)).
    • Spy: 일반적으로 인스턴스에서 생성됩니다 (Mockito.spy(new ArrayList<>())).
  4. 사용 사례:

    • Mock: 동작을 완전히 제어하고 실제 메서드가 필요하지 않은 경우.
    • Spy: 실제 메서드를 사용하고 싶지만 특정 동작만 재정의해야 하는 경우.

실용적인 예: 스파이를 사용해야 하는 경우

스파이의 일반적인 사용 사례인 복잡한 객체를 부분적으로 스텁하는 방법을 보여주는 테스트를 하나 더 추가해 보겠습니다.

@Test
public void testWhenToUseSpy() {
    // Create a complex data object that we want to partially mock
    StringBuilder builder = new StringBuilder();
    StringBuilder spyBuilder = Mockito.spy(builder);

    // Use the real append method
    spyBuilder.append("Hello");
    spyBuilder.append(" World");

    // Verify the real methods were called
    Mockito.verify(spyBuilder).append("Hello");
    Mockito.verify(spyBuilder).append(" World");

    // The real methods modified the state
    assertEquals("Hello World", spyBuilder.toString());
    System.out.println("When to Use Spy - Content: " + spyBuilder.toString());

    // Stub the toString() method to return something else
    Mockito.when(spyBuilder.toString()).thenReturn("Stubbed String");

    // Now toString() returns our stubbed value
    assertEquals("Stubbed String", spyBuilder.toString());
    System.out.println("When to Use Spy - After Stubbing toString(): " + spyBuilder.toString());

    // But other methods still work normally
    spyBuilder.append("!");
    Mockito.verify(spyBuilder).append("!");

    // toString() still returns our stubbed value
    assertEquals("Stubbed String", spyBuilder.toString());
}

업데이트된 코드를 컴파일해 보겠습니다.

cd ~/project
javac -cp lib/junit.jar:lib/mockito-core.jar:lib/byte-buddy.jar:lib/byte-buddy-agent.jar:lib/objenesis.jar:lib/hamcrest-core.jar MockitoSpyDemo.java

코드는 오류 없이 컴파일되어야 합니다. 이 단계에서는 모의 객체와 스파이의 주요 차이점과 각 객체를 사용해야 하는 경우를 배웠습니다. 모의 객체는 원래 객체의 동작을 완전히 대체하는 반면, 스파이는 원래 동작을 유지하면서 필요에 따라 특정 메서드를 재정의할 수 있도록 합니다.

요약

이 랩에서는 Java 단위 테스트를 위한 강력한 기능인 Mockito 의 스파이 (spy) 개념을 살펴보았습니다. 다음은 우리가 다룬 주요 내용입니다.

  1. 스파이 생성: Mockito.spy() 메서드와 @Spy 어노테이션을 사용하여 스파이를 생성하는 방법을 배웠습니다. 전자는 더 유연하고, 후자는 스파이가 많은 경우 더 편리합니다.

  2. 스파이 동작: 스파이는 모의 객체 (mock) 처럼 메서드 호출을 추적하지만 기본적으로 실제 메서드를 실행합니다. 이러한 이중적인 특성으로 인해 상호 작용을 검증할 수 있으면서도 실제 객체를 테스트하는 데 특히 유용합니다.

  3. 스파이 스텁 (stub) 하기: doReturn(), doThrow() 및 기타 스텁 메서드를 사용하여 스파이 메서드의 기본 동작을 재정의하는 방법을 배웠습니다. 이를 통해 특정 동작을 제어하면서 다른 동작은 그대로 유지할 수 있습니다.

  4. 예외 처리: 일반 객체에서 Mockito 검증 메서드를 사용하려고 할 때 발생하는 NotAMockException을 살펴보고 이를 적절하게 처리하는 방법을 배웠습니다.

  5. Mock vs Spy: 모의 객체와 스파이를 비교하여 주요 차이점을 이해했습니다. 모의 객체는 기본 또는 스텁된 응답으로 원래 동작을 완전히 대체하는 반면, 스파이는 원래 동작을 유지하면서 특정 메서드를 재정의할 수 있습니다.

스파이는 Mockito 의 강력한 도구로, 모의 객체의 완전한 제어와 실제 객체의 현실감 사이의 균형을 제공합니다. 다음과 같은 경우에 특히 유용합니다.

  • 실제 객체를 테스트하고 특정 상호 작용을 검증해야 하는 경우
  • 나머지 객체의 동작을 그대로 유지하면서 특정 메서드만 재정의해야 하는 경우
  • 모든 메서드를 모의하는 것이 비실용적인 복잡한 객체로 작업하는 경우

스파이를 언제, 어떻게 사용해야 하는지 이해함으로써 Java 애플리케이션에 대해 보다 효과적이고 유지 관리 가능한 단위 테스트를 작성할 수 있습니다.