본문 바로가기

JAVA

Spock Framework 의 Stub, Mock, Spy 를 간단하게 알아 보자.

Java 는 현재 Class 와 Interface 를 Mocking 하기 위한 다양한 Library 와 Framework 를 가지고 있습니다. Spock 역시 그러한 Framework 중 하나 입니다.

이 문서에서는 Spock 의 다양한 기능 중 Stub, Mock, Spy 에 대해 얇게 알아 보겠습니다.

Spock is a testing and specification framework for Java and Groovy applications. What makes it stand out from the crowd is its beautiful and highly expressive specification language. Thanks to its JUnit runner, Spock is compatible with most IDEs, build tools, and continuous integration servers. Spock is inspired from JUnit, jMock, RSpec, Groovy, Scala, Vulcans, and other fascinating life forms.

Stub

  • Class 가 Dummy 처럼 동작하도록 해야 할때 사용합니다.
public class Item {
    private final String id;
    private final String name;


    // 기본적인 생성자, getter, equals 는 구현되어 있다고 가정
}


// 메소드가 하나 있는 Interface 를 생성함.
public interface ItemProvider {
    List<Item> getItems(List<String>) itemIds);
}


// 테스트를 진행할 Class 를 생성함.
public class ItemService {
    private final ItemProvider itemProvider;
    public ItemService(ItemProvider itemProvider) {
        this.itemProvider = itemProvider;
    }


    List<Item> getAllItemsSortedByName(List<String> itemIds) {
        List<Item> items = itemProvider.getItems(itemIds);
        return items.stream()
                    .sorted(Comparator.Comparing(Item::getName))
                    .collect(Collectors.toList());
    }
}

위의 코드에서 우리는 getAllItemsSortedByName() 메서드의 로직만 테스트를 하길 원합니다. 아래 코드 처럼 테스트 할 수 있습니다.

class ItemServiceMockTest extends Specification {
    ItemProvider itemProvider
    ItemService itemService


    def setup() {
        itemProvider = Stub()
        itemService = new ItemService(itemProvider)
    }


    def '이름으로 정렬된 아이템을 반환'() {
        given:
        def ids = ['offer-id', 'offer-id-2']
        itemProvider.getItems(ids) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')]


        when:
        List<Item> items = itemService.getAllItemsSortedByName(ids)


        then:
        items.collect { it.name } == ['Aname', 'Zname']
    }
}

Mock

  • Stub 과 유사하나, Mock 으로 지정된 클래스는 메서드가 지정된 Argument와 함께 몇번 호출되었는지 확인할 수 있음.
class ItemServiceMockTest extends Specification {
    ItemProvider itemProvider
    ItemService itemService


    def setup() {
        itemProvider = Mock()
        itemService = new ItemService(itemProvider)
    }


    def '이름으로 정렬된 아이템을 반환'() {
        given:
        def ids = ['offer-id', 'offer-id-2']


        when:
        List<Item> items = itemService.getAllItemsSortedByName(ids)


        then:
        items.collect { it.name } == ['Aname', 'Zname']
        1 * itemProvider.getItems(ids) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')]
        // ids 을 argument로 하여 1회 호출
        // 1회 호출한 것이 아닐 경우 failed 처리됨
    }
}

Spy

  • Class 의 일부 Method 만 Mock 처럼 사용하길 원할때 사용합니다.
  • Mock 처럼 사용하길 원하는 Method 는 public 이나 protected 여야 합니다.
public interface EventPublisher {
    void publish(String addedOfferId);
}


public class LoggingEventPublisher implements EventPublisher {
    @Override
    public void publish(String addedOfferId) {
        System.out.println("I've published: " + addedOfferId);
    }
}


public class ItemService {
    private final ItemProvider itemProvider;
    Private final EventPublisher eventPublisher;
    
    public ItemService(ItemProvider itemProvider, EventPublisher eventPublisher) {
        this.itemProvider = itemProvider;
        this.eventPublisher = eventPublisher;
    }


    void saveItems(List<String> itemIds) {
        List<String> notEmptyOfferIds = itemIds.stream()
                                               .filter(itemId -> !itemId.isEmpty())
                                               .collect(Collectors.toList());
        
        // 데이터베이스에 저장하는 코드는 생략 

        notEmptyOfferIds.forEach(eventPublisher::publish);
    }
}


class ItemServiceMockTest extends Specification {
    ItemProvider itemProvider
    ItemService itemService
    EventPublisher eventPublisher

    def setup() {
        itemProvider = Stub()
        eventPublisher = Spy(LoggingEventPublisher)
        itemService = new ItemService(itemProvider, eventPublisher)
    }


    def "아이템 저장 후 publish"() {
        given:
        
        when:
        itemService.saveItems(['item-id'])

        then:
        1 * eventPublisher.publish('item-id') // 1번만호출 되는지 확인 및 실제 method가 호출됨.
        // 1 * eventPublisher.publish('item-id') >> {} 1번만 호출 되는지 확인 및 Mock method 가 호출됨.
    }
}