diff --git a/pom.xml b/pom.xml index d8032b3..98063d0 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,7 @@ test-embedded-kafka kinesis-produce-consume kstream + testing diff --git a/testing/README.adoc b/testing/README.adoc new file mode 100644 index 0000000..f58e331 --- /dev/null +++ b/testing/README.adoc @@ -0,0 +1,3 @@ +== How to test Spring Cloud Stream applications? + +This project contains a set of tests for simple Spring Cloud Stream applications to demonstrate what, how and when we can use to test this kind of microservices. diff --git a/testing/pom.xml b/testing/pom.xml new file mode 100644 index 0000000..c8e93dd --- /dev/null +++ b/testing/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + spring-cloud-stream-sample-testing + jar + + testing + Demonstrating how to test spring cloud stream apps + + + org.springframework.cloud + spring-cloud-stream-samples + 1.2.0.BUILD-SNAPSHOT + + + + + org.springframework.cloud + spring-cloud-stream-binder-kafka + + + + org.springframework.integration + spring-integration-jdbc + + + + org.springframework.cloud + spring-cloud-stream-test-support + test + + + + org.springframework.integration + spring-integration-test + test + + + + org.hsqldb + hsqldb + test + + + + org.springframework.kafka + spring-kafka-test + test + + + + + diff --git a/testing/src/main/java/org/springframework/cloud/stream/testing/processor/ToUpperCaseProcessor.java b/testing/src/main/java/org/springframework/cloud/stream/testing/processor/ToUpperCaseProcessor.java new file mode 100644 index 0000000..41b6f81 --- /dev/null +++ b/testing/src/main/java/org/springframework/cloud/stream/testing/processor/ToUpperCaseProcessor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.testing.processor; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.annotation.StreamListener; +import org.springframework.cloud.stream.messaging.Processor; +import org.springframework.messaging.handler.annotation.SendTo; + +/** + * @author Artem Bilan + * + */ +@SpringBootApplication +@EnableBinding(Processor.class) +public class ToUpperCaseProcessor { + + @StreamListener(Processor.INPUT) + @SendTo(Processor.OUTPUT) + public String transform(String payload) { + return payload.toUpperCase(); + } + + public static void main(String[] args) { + SpringApplication.run(ToUpperCaseProcessor.class, args); + } + +} diff --git a/testing/src/main/java/org/springframework/cloud/stream/testing/sink/JdbcSink.java b/testing/src/main/java/org/springframework/cloud/stream/testing/sink/JdbcSink.java new file mode 100644 index 0000000..121c4d3 --- /dev/null +++ b/testing/src/main/java/org/springframework/cloud/stream/testing/sink/JdbcSink.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.testing.sink; + +import javax.sql.DataSource; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.messaging.Sink; +import org.springframework.context.annotation.Bean; +import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.integration.jdbc.JdbcMessageHandler; +import org.springframework.messaging.MessageHandler; + +/** + * @author Artem Bilan + * + */ +@SpringBootApplication +@EnableBinding(Sink.class) +public class JdbcSink { + + @Bean + @ServiceActivator(inputChannel = Sink.INPUT) + public MessageHandler logHandler(DataSource dataSource) { + return new JdbcMessageHandler(dataSource, "INSERT INTO foobar (value) VALUES (:payload)"); + } + + public static void main(String[] args) { + SpringApplication.run(JdbcSink.class, args); + } + +} diff --git a/testing/src/main/java/org/springframework/cloud/stream/testing/source/FooBarSource.java b/testing/src/main/java/org/springframework/cloud/stream/testing/source/FooBarSource.java new file mode 100644 index 0000000..a960f5e --- /dev/null +++ b/testing/src/main/java/org/springframework/cloud/stream/testing/source/FooBarSource.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.testing.source; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.messaging.Source; +import org.springframework.context.annotation.Bean; +import org.springframework.integration.annotation.InboundChannelAdapter; +import org.springframework.integration.annotation.Poller; +import org.springframework.integration.core.MessageSource; +import org.springframework.messaging.support.GenericMessage; + +/** + * @author Artem Bilan + * + */ +@SpringBootApplication +@EnableBinding(Source.class) +public class FooBarSource { + + private AtomicBoolean semaphore = new AtomicBoolean(true); + + @Bean + @InboundChannelAdapter(channel = Source.OUTPUT, poller = @Poller(fixedDelay = "100")) + public MessageSource fooBarStrings() { + return () -> + new GenericMessage<>(this.semaphore.getAndSet(!this.semaphore.get()) ? "foo" : "bar"); + } + + public static void main(String[] args) { + SpringApplication.run(FooBarSource.class, args); + } + +} diff --git a/testing/src/test/java/org/springframework/cloud/stream/testing/processor/NaiveToUpperCaseTests.java b/testing/src/test/java/org/springframework/cloud/stream/testing/processor/NaiveToUpperCaseTests.java new file mode 100644 index 0000000..2367ebf --- /dev/null +++ b/testing/src/test/java/org/springframework/cloud/stream/testing/processor/NaiveToUpperCaseTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.testing.processor; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * @author Artem Bilan + * + */ +public class NaiveToUpperCaseTests { + + @Test + public void testUpperCase() { + assertEquals("FOO", new ToUpperCaseProcessor().transform("foo")); + } + +} diff --git a/testing/src/test/java/org/springframework/cloud/stream/testing/processor/ToUpperCaseProcessorTests.java b/testing/src/test/java/org/springframework/cloud/stream/testing/processor/ToUpperCaseProcessorTests.java new file mode 100644 index 0000000..96fe5ec --- /dev/null +++ b/testing/src/test/java/org/springframework/cloud/stream/testing/processor/ToUpperCaseProcessorTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.testing.processor; + + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.cloud.stream.test.matcher.MessageQueueMatcher.receivesPayloadThat; + +import java.util.concurrent.BlockingQueue; + +import org.junit.Test; +import org.junit.runner.RunWith; + +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.messaging.Processor; +import org.springframework.cloud.stream.test.binder.MessageCollector; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author Artem Bilan + * + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@DirtiesContext +public class ToUpperCaseProcessorTests { + + @Autowired + private Processor channels; + + @Autowired + private MessageCollector collector; + + @SpyBean + private ToUpperCaseProcessor toUpperCaseProcessor; + + @Test + public void testMessages() { + this.channels.input().send(new GenericMessage<>("foo")); + this.channels.input().send(new GenericMessage<>("bar")); + this.channels.input().send(new GenericMessage<>("foo meets bar")); + this.channels.input().send(new GenericMessage<>("nothing but the best test")); + + BlockingQueue> messages = this.collector.forChannel(channels.output()); + + assertThat(messages, receivesPayloadThat(is("FOO"))); + assertThat(messages, receivesPayloadThat(is("BAR"))); + assertThat(messages, receivesPayloadThat(is("FOO MEETS BAR"))); + assertThat(messages, receivesPayloadThat(not("nothing but the best test"))); + + verify(this.toUpperCaseProcessor, times(4)).transform(anyString()); + } + +} diff --git a/testing/src/test/java/org/springframework/cloud/stream/testing/processor/integration/ToUpperCaseProcessorIntTests.java b/testing/src/test/java/org/springframework/cloud/stream/testing/processor/integration/ToUpperCaseProcessorIntTests.java new file mode 100644 index 0000000..6235f1f --- /dev/null +++ b/testing/src/test/java/org/springframework/cloud/stream/testing/processor/integration/ToUpperCaseProcessorIntTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.testing.processor.integration; + + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Iterator; + +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.stream.testing.processor.ToUpperCaseProcessor; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.test.rule.KafkaEmbedded; +import org.springframework.kafka.test.utils.KafkaTestUtils; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author Artem Bilan + * + */ +@RunWith(SpringRunner.class) +@SpringBootTest( + properties = { + "spring.autoconfigure.exclude=org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration", + "spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer", + "spring.cloud.stream.bindings.output.producer.headerMode=raw", + "spring.cloud.stream.bindings.input.consumer.headerMode=raw", + "spring.cloud.stream.bindings.input.group=embeddedKafkaApplication", + "spring.kafka.consumer.group-id=EmbeddedKafkaIntTest" + }, + classes = ToUpperCaseProcessor.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +@DirtiesContext +public class ToUpperCaseProcessorIntTests { + + @ClassRule + public static KafkaEmbedded kafkaEmbedded = new KafkaEmbedded(1, true, "output"); + + @Autowired + private KafkaTemplate template; + + @Autowired + private DefaultKafkaConsumerFactory consumerFactory; + + @BeforeClass + public static void setup() { + System.setProperty("spring.kafka.bootstrap-servers", kafkaEmbedded.getBrokersAsString()); + System.setProperty("spring.cloud.stream.kafka.binder.zkNodes", kafkaEmbedded.getZookeeperConnectionString()); + } + + @Test + public void testMessagesOverKafka() throws Exception { + this.template.send("input", "bar".getBytes()); + + Consumer consumer = this.consumerFactory.createConsumer(); + + kafkaEmbedded.consumeFromAnEmbeddedTopic(consumer, "output"); + + ConsumerRecords replies = KafkaTestUtils.getRecords(consumer); + assertThat(replies.count()).isEqualTo(1); + + Iterator> iterator = replies.iterator(); + assertThat(iterator.next().value()).isEqualTo("BAR"); + } + +} diff --git a/testing/src/test/java/org/springframework/cloud/stream/testing/sink/JdbcSinkTests.java b/testing/src/test/java/org/springframework/cloud/stream/testing/sink/JdbcSinkTests.java new file mode 100644 index 0000000..02e27f5 --- /dev/null +++ b/testing/src/test/java/org/springframework/cloud/stream/testing/sink/JdbcSinkTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.testing.sink; + + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.stream.messaging.Sink; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author Artem Bilan + * + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@DirtiesContext +public class JdbcSinkTests { + + @Autowired + private Sink channels; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Test + public void testMessages() { + this.channels.input().send(new GenericMessage<>("foo")); + this.channels.input().send(new GenericMessage<>("bar")); + + List> data = this.jdbcTemplate.queryForList("SELECT * FROM foobar"); + assertThat(data.size()).isEqualTo(2); + assertThat(data.get(0).get("value")).isEqualTo("foo"); + assertThat(data.get(1).get("value")).isEqualTo("bar"); + } + +} diff --git a/testing/src/test/java/org/springframework/cloud/stream/testing/source/FooBarSourceTests.java b/testing/src/test/java/org/springframework/cloud/stream/testing/source/FooBarSourceTests.java new file mode 100644 index 0000000..caeabdd --- /dev/null +++ b/testing/src/test/java/org/springframework/cloud/stream/testing/source/FooBarSourceTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.stream.testing.source; + + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.verify; +import static org.springframework.cloud.stream.test.matcher.MessageQueueMatcher.receivesPayloadThat; + +import java.util.concurrent.BlockingQueue; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.stream.messaging.Source; +import org.springframework.cloud.stream.test.binder.MessageCollector; +import org.springframework.messaging.Message; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author Artem Bilan + * + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@DirtiesContext +public class FooBarSourceTests { + + @Autowired + private Source channels; + + @Autowired + private MessageCollector collector; + + @Test + public void testMessages() { + BlockingQueue> messages = this.collector.forChannel(channels.output()); + + assertThat(messages, receivesPayloadThat(is("foo"))); + assertThat(messages, receivesPayloadThat(is("bar"))); + assertThat(messages, receivesPayloadThat(is("foo"))); + assertThat(messages, receivesPayloadThat(is("bar"))); + } + +} diff --git a/testing/src/test/resources/schema.sql b/testing/src/test/resources/schema.sql new file mode 100644 index 0000000..fccb0a5 --- /dev/null +++ b/testing/src/test/resources/schema.sql @@ -0,0 +1,3 @@ +CREATE TABLE FOOBAR ( + value VARCHAR(256), +);