Test code 작성해보기

kiyoung·2024년 1월 19일

spring cloud stream

목록 보기
6/8

spring cloud stream으로 테스트 코드를 작성해 봅니다.

spring cloud stream 3.2.x 버전대까지는 spring-cloud-stream-test-support를 사용하고 4.0 이상부터는 spring-cloud-stream-test-binder를 사용합니다.

Annotation 방식으로 구현된 프로젝트에서 spring-cloud-stream-test-support로 테스트 케이스를 작성해 보고,

Functional 방식으로 구현된 프로젝트에서 dependencies의 버전을 조정한 다음 spring-cloud-stream-test-binder로 테스트 케이스를 작성해 보도록 하겠습니다.


spring-cloud-stream-test-support

MessageChannelsend() 메소드를 호출하고
전달받은 메시지를 확인하거나(메시지 발행),
전달 된 이후 StreamListener가 호출되었는지(메시지 소비) 확인하는 방식으로 작성합니다.

Producer 애플리케이션의 테스트 케이스 작성을 해보겠습니다.

build.gradlespring-cloud-stream-test-support 라이브러리를 추가합니다.

build.gradle

...

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	// spring cloud stream rabbitmq
	implementation 'org.springframework.cloud:spring-cloud-starter-stream-rabbit:3.2.6'
	// 추가
    testImplementation 'org.springframework.cloud:spring-cloud-stream-test-support:3.2.6'
}

...

메시지 발행 테스트를 하기 위해서 MessagePublishTests를 테스트 패키지에 생성합니다.

MessageChannelsend() 메소드를 이용해서 메시지를 보낸 다음에
MessageCollector를 이용해서 MessageChannel에서 보내진 메시지를 가져옵니다.
가져온 메시지는 MessageConverter를 이용해서 MyMessage로 변환되고 이 값이 처음에 보냈던 payload와 일치하는지 검증합니다.

src/test/java
com.github.questcollector.producer
MessagePublishTests.java

package com.github.questcollector.producer;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.support.MessageBuilder;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class MessagePublishTests {
    @Autowired
    private ProducerEventMessageChannels producerEventMessageChannels;

    @Autowired
    private MessageCollector messageCollector;

    @Test
    void publishMessageTest() {
        // payload 생성
        MyMessage payload = new MyMessage(1L, "test");

        // MessageChannel을 통해 메시지 전송
        MessageChannel messageChannel = producerEventMessageChannels.producerPublish();
        messageChannel.send(MessageBuilder.withPayload(payload).build());

        // 전송된 메시지를 가져와서 payload 변환
        Message<?> message = messageCollector.forChannel(messageChannel).poll();
        MessageConverter converter = new MappingJackson2MessageConverter();
        MyMessage receivedPayload = (MyMessage) converter.fromMessage(message, MyMessage.class);

        // 값 검증
        assertThat(receivedPayload).isEqualTo(payload);
    }
}

메시지 수신 테스트의 과정은 아래와 같습니다.
MessageChannel에 메시지가 들어오는 것을 가정하고 send() 메소드로 메시지를 보냅니다.
메시지가 들어왔을 때 StreamListener로 지정했던 consumeEvent() 메소드가 실행되는지 검증하고,
인자로 받은 메시지 데이터를 확인하여 보냈던 값과 같은지 검증합니다.

메시지 수신 테스트는 MessageConsumeTests에 작성합니다.

src/test/java
com.github.questcollector.producer
MessagePublishTests.java

package com.github.questcollector.producer;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.MessageBuilder;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class MessageConsumeTests {

    @Autowired
    private ProducerEventMessageChannels producerEventMessageChannels;

    @SpyBean
    private ProducerMessageListener producerMessageListener;

    @Captor
    private ArgumentCaptor<MyMessage> captor;

    @Test
    void consumeMessageTest() {
        // payload 생성
        MyMessage payload = new MyMessage(1L, "test");

        // MessageChannel에 메시지가 들어온 상태 생성
        SubscribableChannel messageChannel = producerEventMessageChannels.producerConsume();
        messageChannel.send(MessageBuilder.withPayload(payload).build());

        // StreamListener의 실행 확인 및 argument 캡쳐
        verify(producerMessageListener).consumeEvent(captor.capture());

        // argument 값 검증
        assertThat(captor.getValue()).isEqualTo(payload);
    }
}

Receiver 애플리케이션의 경우 메시지를 받고 전송하는 과정이 하나의 StreamListener에 구현되어 있으므로 테스트 케이스를 하나를 생성해 보도록 하겠습니다.

build.gradlespring-cloud-stream-test-support 라이브러리를 추가합니다.

...
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	// spring cloud stream rabbitmq
	implementation 'org.springframework.cloud:spring-cloud-starter-stream-rabbit:3.2.6'
	testImplementation 'org.springframework.cloud:spring-cloud-stream-test-support:3.2.6'
}
...

테스트 패키지에 ReceiverMessageTests를 생성하고 테스트 케이스를 작성합니다.
MessageChannelsend() 메소드를 이용해서 메시지를 전송받은 케이스를 가정하고
StreamListener로 지정된 consumeEvent() 메소드가 호출되었는지 검증하고 argument를 캡쳐하여 검증합니다.
StreamListener에서는 content에 "2" 값을 추가해서 다시 메시를 전송하기 때문에 MessageCollectorMessageChannel에 보내진 메시지를 가져온 다음 값을 검증합니다.

src/test/java
com.github.questcollector.receiver
ReceiverMessageTests.java

package com.github.questcollector.receiver;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.messaging.Message;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.support.MessageBuilder;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class ReceiverMessageTests {
    @Autowired
    private ReceiverEventMessageChannels receiverEventMessageChannels;
    @Autowired
    private MessageCollector messageCollector;
    @SpyBean
    private ReceiverMessageListener receiverMessageListener;
    @Captor
    private ArgumentCaptor<MyMessage> captor;

    @Test
    void messageHandleTest() {
        // payload 구성
        MyMessage payload = new MyMessage(1L, "test");

        // MessageChannel에 메시지가 들어온 상태 생성
        SubscribableChannel messageChannel = receiverEventMessageChannels.receiverConsume();
        messageChannel.send(MessageBuilder.withPayload(payload).build());

        // StreamListener 실행 및 argument 캡쳐
        verify(receiverMessageListener).consumeEvent(captor.capture());

        // argument로 들어온 값 검증
        assertThat(captor.getValue()).isEqualTo(payload);

        // receivePublish로 전송된 메시지 가져오기
        Message<?> message = messageCollector.forChannel(receiverEventMessageChannels.receiverPublish()).poll();
        MessageConverter converter = new MappingJackson2MessageConverter();
        MyMessage received = (MyMessage) converter.fromMessage(message, MyMessage.class);

        // 전송된 메시지 검증
        assertThat(received)
                .hasFieldOrPropertyWithValue("id", 1L)
                .hasFieldOrPropertyWithValue("content", "test2");
    }
}

spring-cloud-stream-test-binder

spring-cloud-stream-test-binder에서는 InputDestinationOutputDestination을 이용해서 MessageChannel에 메시지를 전송하거나 받을 수 있습니다.

spring-cloud-stream-test-binder는 spring cloud stream 4.0 버전 이상에서 만들어진 라이브러리이므로 spring cloud stream의 버전과 이에 맞추어 spring boot의 버전도 조정하도록 하겠습니다.

https://github.com/spring-cloud/spring-cloud-release/wiki/Supported-Versions

build.gradle에서 버전 정보를 수정하고 spring-cloud-stream-test-binder를 추가합니다.

build.gradle

plugins {
	id 'java'
    // 버전 변경
	id 'org.springframework.boot' version '3.1.7'
    // 버전 변경
	id 'io.spring.dependency-management' version '1.1.4'
}

...

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	// spring cloud stream rabbitmq
    // 버전 변경
	implementation 'org.springframework.cloud:spring-cloud-starter-stream-rabbit:4.0.4'
    // 추가
	testImplementation 'org.springframework.cloud:spring-cloud-stream-test-binder:4.0.4'
}

...

메시지 발행이 잘 되는지 테스트하기 위해 MessagePublishTests를 작성합니다.
기존과 비슷하지만 MessageChannel을 직접적으로 사용하지 않고 StreamBridgeOutputDestination을 이용하여 메시지를 포착하여 검증합니다.

src/test/java
com.github.questcollector.producer
MessagePublishTests.java

package com.github.questcollector.producer;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.function.StreamBridge;

import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class MessagePublishTests {

    @Autowired
    private StreamBridge streamBridge;

    @Autowired
    private OutputDestination outputDestination;

    @Test
    void publishMessageTest() throws IOException {
        // payload 구성
        MyMessage payload = new MyMessage(1L, "test");

        // message 전송
        streamBridge.send("producerPublish-out-0", payload);

        // message 확인
        byte[] received = outputDestination.receive(10L, "test1").getPayload();
        ObjectMapper objectMapper = new ObjectMapper();
        MyMessage receivedPayload = objectMapper.reader().readValue(received, MyMessage.class);
        assertThat(receivedPayload).isEqualTo(payload);
    }
}

OutputDestination.receive() 메소드를 통해서 받는 Message는 Message<byte[]> 타입입니다.
byte[]의 payload를 ObjectMapper를 이용해서 MyMessage로 변환한 다음 값을 검증합니다.

한편 OutputDestination.receive() 메소드의 두 번째 파라미터는 bindingName인데, binding에 destination을 지정했다면 destination 값을 입력해야 합니다.

다음으로 메시지가 잘 받아지는지 확인하기 위해 MessageConsumeTests를 작성합니다.

src/test/java
com.github.questcollector.producer
MessageConsumeTests.java

package com.github.questcollector.producer;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.messaging.support.MessageBuilder;

import java.util.function.Consumer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class MessageConsumeTests {
    @Autowired
    private InputDestination inputDestination;

    @MockBean
    private Consumer<MyMessage> producerConsume;

    @Captor
    private ArgumentCaptor<MyMessage> captor;

    @Test
    void consumeMessageTest() {
        // payload 구성
        MyMessage payload = new MyMessage(1L, "test");

        // MessageChannel에 메시지가 들어온 상태 생성
        inputDestination.send(MessageBuilder.withPayload(payload).build(),
                "test2");

        // listener 호출 검증
        verify(producerConsume).accept(captor.capture());

        // argument 검증
        assertThat(captor.getValue()).isEqualTo(payload);
    }
}

inputDestination.send()를 이용해서 메시지를 보내면 MockBean으로 등록한 Consumer<MyMessage>accept() 메소드가 호출되는지 검증합니다.
그리고 argument로 들어온 payload의 값을 검증합니다.
Consumer의 로직이 단순하기 때문에 따로 검증하지는 않았지만, 다양한 동작을 하도록 구현하였다면 이 부분도 테스트해야 할 것입니다.

Receiver 애플리케이션의 테스트 케이스도 작성해 봅니다.

마찬가지로 build.gradle에서 spring boot의 버전과 spring cloud stream의 버전을 수정하고 test binder 라이브러리를 추가합니다.

build.gradle

plugins {
	id 'java'
	// 버전 변경
	id 'org.springframework.boot' version '3.1.7'
	// 버전 변경
	id 'io.spring.dependency-management' version '1.1.4'
}

...

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	// spring cloud stream rabbitmq
	// 버전 변경
    implementation 'org.springframework.cloud:spring-cloud-starter-stream-rabbit:4.0.4'
    // 추가
	testImplementation 'org.springframework.cloud:spring-cloud-stream-test-binder:4.0.4'
}

...

binding을 통해 메시지를 잘 주고 받고 있는지 확인하는 ReceiverBindingTests을 작성합니다.

src/test/java
com.github.questcollector.receiver
ReceiverBindingTests.java

package com.github.questcollector.receiver;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.messaging.support.MessageBuilder;

import java.io.IOException;
import java.util.function.Function;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class ReceiverBindingTest {

    @Autowired
    private InputDestination inputDestination;
    @Autowired
    private OutputDestination outputDestination;
    @Autowired
    private StreamBridge streamBridge;
    @MockBean
    private Function<MyMessage, MyMessage> receiverMessageHandler;

    @Test
    void consumeMessageTest() {
        // payload 구성
        MyMessage payload = new MyMessage(1L, "test");

        // receiverMessageHandler mocking
        given(receiverMessageHandler.apply(any(MyMessage.class)))
                .willReturn(new MyMessage(1L, "test2"));

        // MessageChannel에 메시지가 들어온 상태 생성
        inputDestination.send(MessageBuilder.withPayload(payload).build());

        // receiverMessageHandler 호출 검증
        verify(receiverMessageHandler).apply(payload);

    }
    @Test
    void publishMessageTest() throws IOException {
        // payload 구성
        MyMessage payload = new MyMessage(1L, "test");

        // 메시지 발행
        streamBridge.send("receiverMessageHandler-out-0", payload);

        // Message 확인
        byte[] received = outputDestination.receive(10L, "test2").getPayload();
        ObjectMapper objectMapper = new ObjectMapper();
        MyMessage receivedPayload = objectMapper.reader().readValue(received, MyMessage.class);
        assertThat(receivedPayload).isEqualTo(payload);
    }
}

Bean으로 등록했던 Function의 기능을 테스트 하기 위해서 전체 Spring Context를 이용하는 무거운 @SpringBootTest를 활용할 필요는 없습니다.
Function의 단위 테스트를 하기 위해 ReceiverMessageHandlerTests를 작성합니다.

src/test/java
com.github.questcollector.receiver
ReceiverMessageHandlerTests.java

package com.github.questcollector.receiver;

import org.junit.jupiter.api.Test;

import java.util.function.Function;

import static org.assertj.core.api.Assertions.assertThat;

public class ReceiverMessageHandlerTests {

    @Test
    void receiverMessageHandlerTest() {
        // payload 구성
        MyMessage payload = new MyMessage(1L, "test");

        // Function 호출
        ReceiverMessageListener receiverMessageListener = new ReceiverMessageListener();
        Function<MyMessage, MyMessage> receiverMessageHandler = receiverMessageListener.receiverMessageHandler();
        MyMessage processedPayload = receiverMessageHandler.apply(payload);

        // 결과값 검증
        assertThat(processedPayload)
                .hasFieldOrPropertyWithValue("id", 1L)
                .hasFieldOrPropertyWithValue("content", "test2");

    }
}

0개의 댓글