This document describes a custom JUnit extension which allows for running the same JUnit tests against multiple Kafka cluster configurations.
A new @ClusterTest annotation is introduced which allows for a test to declaratively configure an underlying Kafka cluster.
@ClusterTest def testSomething(): Unit = { ... }
This annotation has fields for a set of cluster types and number of brokers, as well as commonly parameterized configurations. Arbitrary server properties can also be provided in the annotation:
@ClusterTest( types = {Type.KRAFT}, brokerSecurityProtocol = SecurityProtocol.PLAINTEXT, properties = { @ClusterProperty(key = "inter.broker.protocol.version", value = "2.7-IV2"), @ClusterProperty(key = "socket.send.buffer.bytes", value = "10240"), }) void testSomething() { ... }
Multiple @ClusterTest annotations can be given to generate more than one test invocation for the annotated method.
@ClusterTests(Array( new ClusterTest(brokerSecurityProtocol = SecurityProtocol.PLAINTEXT), new ClusterTest(brokerSecurityProtocol = SecurityProtocol.SASL_PLAINTEXT) )) def testSomething(): Unit = { ... }
A class-level @ClusterTestDefaults annotation is added to provide default values for @ClusterTest defined within the class. The intention here is to reduce repetitive annotation declarations and also make changing defaults easier for a class with many test cases.
In order to allow for more flexible cluster configuration, a @ClusterTemplate annotation is also introduced. This annotation takes a single string value which references a static method on the test class. This method is used to produce any number of test configurations using a fluent builder style API.
import java.util.Arrays; @ClusterTemplate("generateConfigs") void testSomething() { ... } static List<ClusterConfig> generateConfigs() { ClusterConfig config1 = ClusterConfig.defaultClusterBuilder() .name("Generated Test 1") .serverProperties(props1) .setMetadataVersion(MetadataVersion.IBP_2_7_IV1) .build(); ClusterConfig config2 = ClusterConfig.defaultClusterBuilder() .name("Generated Test 2") .serverProperties(props2) .setMetadataVersion(MetadataVersion.IBP_2_7_IV2) .build(); ClusterConfig config3 = ClusterConfig.defaultClusterBuilder() .name("Generated Test 3") .serverProperties(props3) .build(); return Arrays.asList(config1, config2, config3); }
This “escape hatch” from the simple declarative style configuration makes it easy to dynamically configure clusters.
One thing to note is that our “test*” methods are no longer tests, but rather they are test templates. We have added a JUnit extension called ClusterTestExtensions which knows how to process these annotations in order to generate test invocations. Test classes that wish to make use of these annotations need to explicitly register this extension:
import org.apache.kafka.common.test.junit.ClusterTestExtensions @ExtendWith(value = Array(classOf[ClusterTestExtensions])) class ApiVersionsRequestTest { ... }
The lifecycle of a test class that is extended with ClusterTestExtensions follows:
@ClusterTest, @ClusterTests, or @ClusterTemplateClusterTestExtensions is called for each of these template methods in order to generate some number of test invocationsFor each generated invocation:
@BeforeAll methods are called@BeforeEach methods are called@AfterEach methods are called@AfterAll methods are called@BeforeEach methods give an opportunity to setup additional test dependencies before the cluster is started.
The class is introduced to provide context to the underlying cluster and to provide reusable functionality that was previously garnered from the test hierarchy.
In order to inject the object, simply add it as a parameter to your test class, @BeforeEach method, or test method.
| Injection | Class | BeforeEach | Test | Notes |
|---|---|---|---|---|
| ClusterInstance | yes* | no | yes | Injectable at class level for convenience, can only be accessed inside test |
@Test will still be run, but no cluster will be started and no dependency injection will happen. This is generally not what you want.