已经到了没有测试案例,就不会写代码的年纪了!不过好在,优秀的框架或组件总是会在介绍完主体功能之后,附带介绍如何进行测试。
然后,因为工作原因,又简单研究了下kafka的测试框架。其中,最神奇的地方是,Spring团队为了便于测试,秉持着能内嵌一定内嵌的原则,搞了一套内嵌的zookeeper和kafka。这样在单元测试期间,就不用开发人员再准备相关环境了。
那么,下面来揭秘下具体是如何实现的:
1. 依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
还是基于Spring框架来研究,我们现在离开了春天框架,就感觉一夜回到解放前,啥都要自己从零开始搭建了。
2. 测试类
首先,看下测试类的定义部分:
@RunWith(SpringRunner.class)
@EmbeddedKafka(topics = "cat")
@ImportAutoConfiguration(KafkaAutoConfiguration.class)
public class SpringBootKafkaTest
很简单的定义,主要包括两部分:
-
@EmbeddedKafka(topics = "cat")
:用来定义内嵌的kafka broker,topics
参数用于指定需要创建的topic; -
ImportAutoConfiguration
:一般测试的时候,我都比较排斥直接使用@SpringBootTest
注解,这玩意儿太重了,把能加载的都加载了,搞得巨慢无比。这里面我们只要加载和kafka相关的部分就行了,也就是加载KafkaAutoConfiguration
就够了;KafkaAutoConfiguration
这个类在上篇介绍过,这里直接忽略;
注意!!!:
KafkaAutoConfiguration
类初始化kafka框架时,是通过参数spring.kafka.bootstrap-servers
确定要连接的kafka broker列表的。如果没有这个参数,则默认使用:localhost:9092
。
上面这点漏掉了,补充一下。如果没有这个信息,后面整个框架都串不起来了。然后spring.kafka.bootstrap-servers
默认值是通过这段设置的,代码贴一下,证明不是我胡说:
再来看一段奇丑无比的静态方法段代码:
public class SpringBootKafkaTest {
static {
System.setProperty(EmbeddedKafkaBroker.BROKER_LIST_PROPERTY, "spring.kafka.bootstrap-servers");
}
设置了一个环境变量,用BROKER_LIST_PROPERTY
常量作为key,保存了spring.kafka.bootstrap-servers
这个值。这个spring.kafka.bootstrap-servers
刚刚说过,保存的是kafka broker的地址列表。
这段代码确实丑!这也不是我说的,因为在后面更高的版本中,这种写法已经被官方自己改掉了。
3. 揭秘开始
好了,现在开始揭秘!
首先,研究下包结构:
看到了一个spring.factories
文件,一定不能放过,看下内容,一行信息:
# Spring Test ContextCustomizerFactories
org.springframework.test.context.ContextCustomizerFactory=\
org.springframework.kafka.test.context.EmbeddedKafkaContextCustomizerFactory
所以,逻辑可能在EmbeddedKafkaContextCustomizerFactory
这里面:
EmbeddedKafkaContextCustomizerFactory
这个类创建了一个EmbeddedKafkaContextCustomizer
对象,并且将@EmbeddedKafka
注解的信息传给了它。
代码较短,贴一下:
@Override
public ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
EmbeddedKafka embeddedKafka =
AnnotatedElementUtils.findMergedAnnotation(testClass, EmbeddedKafka.class);
return embeddedKafka != null ? new EmbeddedKafkaContextCustomizer(embeddedKafka) : null;
}
3.1 EmbeddedKafkaContextCustomizer
这个EmbeddedKafkaContextCustomizer
就是核心逻辑所在的位置了,其中有个customizeContext
方法。
customizeContext
创建了一个EmbeddedKafkaBroker
对象,并将这个对象交给了Spring的IoC容器管理:
// EmbeddedKafkaContextCustomizer.customizeContext中的代码片段
EmbeddedKafkaBroker embeddedKafkaBroker = new EmbeddedKafkaBroker(this.embeddedKafka.count(),
this.embeddedKafka.controlledShutdown(),
this.embeddedKafka.partitions(),
topics);
// EmbeddedKafkaContextCustomizer.customizeContext中的代码片段
beanFactory.initializeBean(embeddedKafkaBroker, EmbeddedKafkaBroker.BEAN_NAME);
这个initializeBean
方法会触发bean对象的afterPropertiesSet
方法执行。然后呢,在EmbeddedKafkaBroker
中,刚好有一个,真是凑巧^.^。
3.2 EmbeddedKafkaBroker
EmbeddedKafkaBroker.afterPropertiesSet
方法主要完成了zookeeper和kafka broker的创建工作:
// EmbeddedKafkaBroker.afterPropertiesSet方法的代码片段
this.zookeeper = new EmbeddedZookeeper();
...
// EmbeddedKafkaBroker.afterPropertiesSet方法的代码片段
KafkaServer server = TestUtils.createServer(new KafkaConfig(brokerConfigProperties), Time.SYSTEM);
...
// EmbeddedKafkaBroker.afterPropertiesSet方法的代码片段
createKafkaTopics(this.topics);
在这个方法的最后,有一段代码:
this.brokerListProperty = System.getProperty(BROKER_LIST_PROPERTY);
if (this.brokerListProperty == null) {
this.brokerListProperty = SPRING_EMBEDDED_KAFKA_BROKERS;
}
System.setProperty(this.brokerListProperty, getBrokersAsString());
这个BROKER_LIST_PROPERTY
的值,前面已经介绍过了,就是spring.kafka.bootstrap-servers
。所以在方法的最后,将kafka broker的地址列表,又写到名称为spring.kafka.bootstrap-servers
的环境变量中。
然后,上面所有的这些方法的执行,都在KafkaAutoConfiguration
相关方法执行之前完成执行的。
所以,在KafkaAutoConfiguration
对kafka框架初始化时,就可以通过spring.kafka.bootstrap-servers
参数,连接到内嵌的kafka集群上来了。完美地衔接上了!
好了,揭秘结束!