Kafka Streams系列中的先前博客文章涵盖了无状态
和有状态的
DSL API中的操作。 在此博客中,我们将探索一些示例,以演示如何使用测试实用程序基于Kafka Streams DSL API验证拓扑。
Kafka Streams提供了测试实用程序,可以为您的流处理管道执行单元测试,而不必依赖外部或嵌入式Kafka集群。 除了测试之外,这些实用程序还可以作为学习各种API功能的绝佳学习工具。
让我们从测试相关API的高级概述开始
Code is available on GitHub和tests can be executed by cloning the repo其次是
mvn test
Key concepts
Initially, there were a few classes in the org.apache.kafka.streams.test
package. 现在不推荐使用以下类
TestInputTopic
的实例TestInputTopic
代表输入主题,您可以使用来向其中发送记录pipeInput
方法(及其重载版本)。 创建TestInputTopic
实例使用拓扑测试驱动程序
(如下所述),并在需要时使用自定义序列化程序。 然后,您可以发送键值对,一次只能发送一个值,也可以批量发送(使用一个清单
)
TestOutputTopic
TestOutputTopic
是发送-接收方程式的另一半,并补充了TestInputTopic
。 您可以使用它来读取拓扑操作写入的输出主题中的记录。 它的方法包括读取记录(键值对),仅读取值,查询大小(尚未使用的当前记录数)等。
拓扑测试驱动程序
拓扑结构TestDriver
包含对拓扑结构
以及与您的Kafka Streams应用程序相关的配置。 如前所述,它用于创建以下对象的实例TestInputTopic
,TestOutputTopic
,提供对州立商店等的访问权限
High level flow
如果您正在使用马文
,您可以将测试实用程序作为依赖项包含在内
org.apache.kafka
kafka-streams-test-utils
2.4.0
test
并且您将(最有可能)使用JUnit的的
和Hamcrest
编写匹配规则...
junit
junit
4.12
test
org.hamcrest
hamcrest-core
1.3
test
这是一个测试用例的外观(类似于您用以下方法对任何Java代码进行单元测试的方式)JUnit
等等。)
- 使用以下命令设置全局状态(如果有)
@课前
注释方法 - 使用的每个测试运行的设置状态
@之前
带注释的方法-通常在此处创建拓扑测试驱动程序
等等 -
@测试
验证方法拓扑结构
-
@后
(和/或@下课以后
)拆除任何状态(全局状态或其他状态)的方法
请确保您致电
拓扑结构TestDriver.close()
清理拓扑中的处理器及其关联状态。 否则可能会由于状态不一致而导致测试失败
现在您已经了解了概念和基本设置,下面让我们看一些具体示例。 我们将从无状态操作开始
Testing stateless操作s
filter
这里是拓扑结构
使用过滤器方法仅允许长度大于5的值。
StreamsBuilder builder = new StreamsBuilder();
KStream stream = builder.stream(INPUT_TOPIC);
stream.filter((k, v) -> v.length() > 5).to(OUTPUT_TOPIC);
这是相应的测试:
@Test
public void shouldIncludeValueWithLengthGreaterThanFive() {
topology = App.retainWordsLongerThan5Letters();
td = new 拓扑测试驱动程序(topology, config);
inputTopic = td.createInputTopic(App.INPUT_TOPIC, Serdes.String().serializer(), Serdes.String().serializer());
outputTopic = td.createOutputTopic(App.OUTPUT_TOPIC, Serdes.String().deserializer(), Serdes.String().deserializer());
assertThat(outputTopic.isEmpty(),is(true));
inputTopic.pipeInput(“ 键1”,“ 酒吧rrrr”);
assertThat(outputTopic.readValue(),equalTo(“ barrrrr”));
assertThat(outputTopic.isEmpty(), is(true));
inputTopic.pipeInput("键2", "bar");
assertThat(outputTopic.isEmpty(), is(true));
}
我们首先选择Topology
我们要测试,创建TopologyTestDriver
实例以及TestInputTopic
和TestOutputTopic
对象。
接下来,我们在发送任何数据之前确认输出主题是否为空-assertThat(outputTopic.isEmpty(), is(true));
现在可以使用以下命令将数据/记录发送到输入主题inputTopic.pipeInput("key1", "barrrrr");
这是一个同步过程,并触发Topology
在这种情况下执行filter
由于值的长度大于5,因此将其推送到输出主题。 我们用相同的方式确认assertThat(outputTopic.readValue(), equalTo("barrrrr"));
并仔细检查输出主题是否为空
最后,我们发送值bar
并确认它没有发送到输出主题,因为它的长度小于5。
flatMap
如本系列第1部分(无状态操作)所述,这是一个flatMap
operation
StreamsBuilder builder = new StreamsBuilder();
KStream stream = builder.stream(INPUT_TOPIC);
stream.flatMap(new 核心价值Mapper>>() {
@Override
public Iterable extends KeyValue extends String, ? extends String>> apply(String k, String csv) {
String[] values = csv.split(",");
return Arrays.asList(values)
.stream()
.map(value -> new KeyValue<>(k, value))
.collect(Collectors.toList());
}
}).to(OUTPUT_TOPIC);
在上面的示例中,流中的每个记录获取flatMap
ped,以便首先将每个CSV(逗号分隔)值拆分为各部分,KeyValue
将为CSV字符串的每个部分创建一对。
为了测试这个。
topology = App.flatMap();
td = new TopologyTestDriver(topology, config);
inputTopic = td.createInputTopic(App.INPUT_TOPIC, Serdes.String().serializer(), Serdes.String().serializer());
outputTopic = td.createOutputTopic(App.OUTPUT_TOPIC, Serdes.String().deserializer(), Serdes.String().deserializer());
inputTopic.pipeInput("随机", "foo,bar,baz");
inputTopic.pipeInput("hello", "world,universe");
inputTopic.pipeInput("hi", "there");
assertThat(outputTopic.getQueueSize(),equalTo(6L));
assertThat(outputTopic.readKeyValue(), equalTo(new KeyValue<>("random", "foo")));
assertThat(outputTopic.readKeyValue(), equalTo(new KeyValue<>("random", "bar")));
assertThat(outputTopic.readKeyValue(), equalTo(new KeyValue<>("random", "baz")));
assertThat(outputTopic.readKeyValue(), equalTo(new KeyValue<>("hello", "world")));
assertThat(outputTopic.readKeyValue(), equalTo(new KeyValue<>("hello", "universe")));
assertThat(outputTopic.readKeyValue(), equalTo(new KeyValue<>("hi", "there")));
assertThat(outputTopic.isEmpty(), is(true));
和往常一样,我们设置所需的测试工具类,并将输入记录推送到输入主题。 例如 对于关键random
及其逗号分隔的值foo,bar,baz
将被拆分为单独的键值对,即它们将导致三条记录被推送到输出表。 其他输入记录也是如此。
我们确认输出主题中的记录数assertThat(outputTopic.getQueueSize(), equalTo(6L));
并验证每个键值对以确认flatMap
行为
Stateful operation without State store
这是使用groupByKey
followed by 计数
并将结果存储在输出主题中
StreamsBuilder builder = new StreamsBuilder();
KStream stream = builder.stream(INPUT_TOPIC);
stream.groupByKey()
.count()
.toStream()
.to(OUTPUT_TOPIC);
测试有状态操作与无状态操作没有太大区别。
topology = App.count();
td = new TopologyTestDriver(topology, config);
inputTopic = td.createInputTopic(App.INPUT_TOPIC, Serdes.String().serializer(), Serdes.String().serializer());
TestOutputTopic ot = td.createOutputTopic(App.OUTPUT_TOPIC, Serdes.String().deserializer(), Serdes.Long().deserializer());
inputTopic.pipeInput("key1", "value1");
inputTopic.pipeInput("key1", "value2");
inputTopic.pipeInput("key2", "value3");
inputTopic.pipeInput("键3", "value4");
inputTopic.pipeInput("key2", "value5");
assertThat(ot.readKeyValue(), equalTo(new KeyValue("key1", 1L)));
assertThat(ot.readKeyValue(), equalTo(new KeyValue("key1", 2L)));
assertThat(ot.readKeyValue(), equalTo(new KeyValue("key2", 1L)));
assertThat(ot.readKeyValue(), equalTo(new KeyValue("key3", 1L)));
assertThat(ot.readKeyValue(), equalTo(new KeyValue("key2", 2L)));
将各个记录发送到输入主题和输出主题,然后验证计数。 不出所料key1
, key2
and key3
分别有2个,2个,1个。
Stateful operation with a State store
当拓扑由状态存储组成时,事情变得很有趣。 在此示例中,不是将counds发送到输出主题,而是使用中间状态存储(可以通过Interactive Queries进行查询)
StreamsBuilder builder = new StreamsBuilder();
KStream stream = builder.stream(INPUT_TOPIC);
stream.groupByKey().count(Materialized.as("count-store"));
TopologyTestDriver提供对状态存储(键值存储
)通过getKeyValueStore
。 在将每条记录发送到输入主题后,将验证状态存储计数。assertThat(countStore.get(“ key1”),equalTo(1L));
topology = App.countWithStateStore();
td = new TopologyTestDriver(topology, config);
inputTopic = td.createInputTopic(App.INPUT_TOPIC, Serdes.String().serializer(), Serdes.String().serializer());
KeyValueStore countStore = td.getKeyValueStore("count-store");
inputTopic.pipeInput("key1", "value1");
assertThat(countStore.get("key1"), equalTo(1L));
inputTopic.pipeInput("key1", "value2");
assertThat(countStore.get("key1"), equalTo(2L));
inputTopic.pipeInput("key2", "value3");
assertThat(countStore.get("key2"), equalTo(1L));
inputTopic.pipeInput("key3", "value4");
assertThat(countStore.get("key3"), equalTo(1L));
inputTopic.pipeInput("key2", "value5");
assertThat(countStore.get("key2"), equalTo(2L));
请注意,在我们的测试中,我们在每个测试方法中都创建了Topology,TopologyTestDriver,TestInputTopic和TestOutputTopic。 这仅仅是因为我们正在测试不同的拓扑。 如果您使用一堆测试用例作为单个JUnit类的一部分来测试单个拓扑,则可以非常轻松地将其移动到带有注释的设置方法中
@之前
这样它就可以在每个测试用例开始之前自动运行
目前为止就这样了! 这是简短但希望有用的介绍,用于测试基于Kafka Streams的处理管道。