创建一个堆栈java
如今的后端工程可能需要集成多种服务。 对于工程师来说,在他们的本地开发环境中安装许多服务是很痛苦的。 Docker提供了一种更简单的方法来执行此操作,但是它仍然需要在我们的代码之外进行一些脚本编写。 如果我们要测试较小的函数或类而不是整个服务,那么它也不是一个完美的解决方案。 这个问题已由Testcontainers [ 1 ]解决。
本文将使您了解什么是Testcontainer,为什么要使用Testcontainers,最后是如何使用它。 尽管本文仅基于Java编程语言的经验,但Testcontainers确实支持其他编程语言,例如go [ 2 ]和node-js [ 3 ]。
什么是测试容器
Testcontainers是一个轻量级的库,可以“包装” docker并连接到测试框架,例如Junit和Spock。 如您在其maven存储库中所见,它也是模块化的。 这意味着,您只能导入对您重要的必要堆栈。
为什么要使用测试容器
测试容器肯定会使软件工程师的生活更简单!
Docker和Docker Compose简化了在本地开发和生产环境中的部署服务。 它们就像轻量级虚拟机,因此您在当前环境中所做的任何操作都不会影响Docker环境中包含的服务。 但是,您需要告诉docker在当前打开的IDE之外运行服务。 使用Tescontainer将使此过程变得更短。 您可以直接在Intellij IDEA中使用ctrl + F10。
使Testcontainers发光的另一个有趣的方面是,它将创建一次性实例。 这意味着您可以在假设每个测试可以独立执行的前提下编写测试。 您可能不想在一项测试中保留插入PostgreSQL数据库中的任何内容。 保留测试记录可能会导致另一个测试失败。 这导致您的测试出现错误。 等待! 您是否正在测试,以便您的主要代码没有错误?
测试容器将使我们能够在一个测试类中使用多个技术堆栈。 例如,您需要测试确实插入到Mysql实例的Kafka使用者。 我们可以在同一个类中定义这两种服务!
如何使用测试容器
尽管Testcontainers可以在Junit运行时中运行,但最好在集成测试中使用。 通常,您希望在几百毫秒内运行一个单元测试类。 在单元测试中运行许多Testcontainer实例将导致单元测试非常慢,并且您不希望每个单元测试运行约20秒。
只需几行
假设您要使用Kafka,可以在测试文件中轻松地将Kafka实例定义为
public class JavaKafkaTest {
@Rule
public KafkaContainer kafka;
@Before
public void setUp () throws Exception {
kafka = new KafkaContainer( "5.4.2" );
kafka.start();
}
}
此代码将以几行代码下载版本5.4.2
(Kafka版本2.5.0
)的Confluent平台的docker映像文件,并启动docker。
Kafka与zookeeper结合使用,但是默认情况下,它已由Testcontainers处理。 如果您出于某种原因希望使用自己的动物园管理员,那么幸运的是有一个选择。
完整示例:使用Kafka流生成消息并使用
我已经向您展示了如何制作KafkaContainer
实例的一些知识,现在我将演示完整的课程。
public class JavaKafkaTest {
@Rule
public KafkaContainer kafka;
@Before
public void setUp () throws Exception {
kafka = new KafkaContainer( "5.4.2" );
kafka.start();
Properties adminProperties = new Properties();
adminProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());
AdminClient adminClient = KafkaAdminClient.create(adminProperties);
adminClient.createTopics(
Stream.of( "plaintext-input" )
.map(n -> new NewTopic(n, 1 , ( short ) 1 ))
.collect(Collectors.toList())
).all().get();
}
@Test
public void testKafkaTestcontainer () throws InterruptedException {
prepareSeedMessages();
final Topology topology = prepareKafkaTopology();
final KafkaStreams streams = new KafkaStreams(topology, consumerProps());
streams.start();
Thread.sleep( 5000 );
}
private void prepareSeedMessages () {
KafkaProducer producer = new KafkaProducer(producerProps());
producer.send( new ProducerRecord( "plaintext-input" , "this is sparta" ));
producer.send( new ProducerRecord( "plaintext-input" , "this is sparta" ));
producer.send( new ProducerRecord( "plaintext-input" , "this is sparta" ));
producer.close();
}
private Topology prepareKafkaTopology () {
final StreamsBuilder streamsBuilder = new StreamsBuilder();
streamsBuilder
.<String, String>stream( "plaintext-input" )
.peek((k, v) -> {
Logger log = Logger.getLogger(Thread.currentThread().getName());
log.info(String.format( "receive message from plaintext-input : %s" , v));
})
.flatMapValues(v -> Arrays.asList(v.split( "\\W+" )))
.peek((k, v) -> {
Logger log = Logger.getLogger(Thread.currentThread().getName());
log.info(String.format( "receive message from count : %s" , v));
})
.groupBy((k, v) -> v)
.count();
return streamsBuilder.build();
}
private Properties consumerProps () {
Properties consumerProps = new Properties();
consumerProps.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
consumerProps.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
consumerProps.put(StreamsConfig.APPLICATION_ID_CONFIG, "sample-app" );
consumerProps.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());
return consumerProps;
}
private Properties producerProps () {
Properties producerProperties = new Properties();
producerProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());
producerProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
producerProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
return producerProperties;
}
@After
public void tearDown () {
kafka.stop();
}
}
在setUp
方法上,准备了所有必需的组件。 必须先启动KafkaContainer
,然后再进行其他调用。 请记住, @Before
将对每个测试方法都执行,这样会使我们的测试不可变。
但是另一方面,这将使容器在测试方法之间上下浮动。
函数testKafkaContainer
是可运行的部分。 在这里您可以做事并断言您需要的所有东西。 本示例演示使用单个方法完成发布和订阅。
prepareSeedMessages
过程将使用三个字符串填充Kafka队列。 我设置了标准的必需属性:密钥序列化程序,值序列化程序,应用程序ID和引导服务器。 重要的是,在设置生产者的属性时,我从Kafka容器实例中获取了一个引导服务器 ,而不是自己对其进行硬编码。
然后,我们准备创建Kafka流的拓扑。 拓扑基本上是有向无环图(DAG),它定义了处理的顺序 [4]。 我将使用“必要的”字数拓扑示例。
在这里,我已定义为从“纯文本输入”主题中获取输入。 我使用peek执行记录过程,并返回该过程收到的内容。 然后, flatMapValues
将使用空格将每个令牌断开。 然后,我可以将每个单词分组并进行计数。
最后,我使用tearDown
方法停止了Kafka容器。 此方法将破坏容器并删除该容器内的所有数据。
如果在一个类中有多个测试,最好将start和stop方法移到@BeforeClass
和@AfterClass
。 像本例一样,使用@Before
和@After
将导致在每次执行单个测试方法时创建和销毁容器。 这会使运行此测试类的时间很长。 相反,我们可以为每个测试类声明一次构造。
结论
在这里,我已经简要说明了Testcontainers是一个封装docker并使它与测试库一起运行的库。 我还列出了使用Testcontainers的一些原因:直接在您的IDE中运行并独立运行测试。
最后,我给出了在Testcontainers中使用Kafka的示例。
参考资料
- [1] Testcontainer文档。 https://www.testcontainers.org/
- [2] testcontainer-go存储库。 https://github.com/testcontainers/testcontainers-go
- [3] testcontainer-node存储库。 https://github.com/testcontainers/testcontainers-node
- [4]优化流。 https://docs.confluent.io/current/streams/developer-guide/optimizing-streams.html
翻译自: https://hackernoon.com/how-to-create-simple-multi-stacks-test-with-testcontainers-tx1u3uj5
创建一个堆栈java