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로 테스트 케이스를 작성해 보도록 하겠습니다.
MessageChannel에 send() 메소드를 호출하고
전달받은 메시지를 확인하거나(메시지 발행),
전달 된 이후 StreamListener가 호출되었는지(메시지 소비) 확인하는 방식으로 작성합니다.
Producer 애플리케이션의 테스트 케이스 작성을 해보겠습니다.
build.gradle에 spring-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를 테스트 패키지에 생성합니다.
MessageChannel에 send() 메소드를 이용해서 메시지를 보낸 다음에
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.gradle에 spring-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를 생성하고 테스트 케이스를 작성합니다.
MessageChannel에 send() 메소드를 이용해서 메시지를 전송받은 케이스를 가정하고
StreamListener로 지정된 consumeEvent() 메소드가 호출되었는지 검증하고 argument를 캡쳐하여 검증합니다.
StreamListener에서는 content에 "2" 값을 추가해서 다시 메시를 전송하기 때문에 MessageCollector로 MessageChannel에 보내진 메시지를 가져온 다음 값을 검증합니다.
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에서는 InputDestination과 OutputDestination을 이용해서 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을 직접적으로 사용하지 않고 StreamBridge와 OutputDestination을 이용하여 메시지를 포착하여 검증합니다.
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");
}
}