Thorough testing is an essential part of delivering software that works. Strimzi is tested extensively using unit, integration and system tests. In this blog post we are going to do a deep dive into the Strimzi system tests for anyone who’s ever wondered how we do it.
Motivation
Maybe you’ve been asking yourself the following questions:
- Why do we need to write system tests?
- Is there any value to system tests?
- Do they catch errors that unit or integration tests don’t?
The answer is very simple. Yes! System tests are a key part of being able to ship Strimzi to users with more confidence that it will not crash in production. The types of tests that validate the basic properties of a system we call Smoke tests Smoke tests detect major issues, such as issues with the deployment of an application or general configuration. Moreover, we have regression test suites. Regression tests cover most features and will find a bug if one is caused by recent changes in verified code. In the next section, I will describe the difference between our system tests, integration, and unit and integration tests.
How system tests differ from our unit and integration tests
Unit tests
First, we need to know the purpose of the unit tests. Let’s define a unit test.
A unit test exercises a single behavior of a software module. That module is usually a class, and the behavior is usually a public method of the class. The test asserts that the actual result matches the expected result. These assertions must all pass, or the unit test will fail. (Ryan Cook)
For clarification, let’s see an example of a unit test for the cluster-operator module.
This is the test case, which verifies that if we set up a Kafka CR with an external
nodeport listener, then the Kafka class instance contains the external port name,
external port 9094 with protocol TCP and more.
Everything that this type of test validates is only a single behaviour of a software module that we defined.
We don’t need a running Kubernetes cluster for unit tests.
It is only in-memory representation of class, in this case the Kafka model.
@Test
public void testExternalNodePorts() {
Kafka kafkaAssembly = new KafkaBuilder(ResourceUtils.createKafka(namespace, cluster, replicas,
image, healthDelay, healthTimeout, metricsCm, configuration, emptyMap()))
.editSpec()
.editKafka()
.withNewListeners()
.withNewKafkaListenerExternalNodePort()
.withNewKafkaListenerAuthenticationTlsAuth()
.endKafkaListenerAuthenticationTlsAuth()
.endKafkaListenerExternalNodePort()
.endListeners()
.endKafka()
.endSpec()
.build();
KafkaCluster kc = KafkaCluster.fromCrd(kafkaAssembly, VERSIONS);
// Check StatefulSet changes
StatefulSet sts = kc.generateStatefulSet(true, null, null);
List<ContainerPort> ports = sts.getSpec().getTemplate().getSpec().getContainers().get(0).getPorts();
assertThat(ports.contains(kc.createContainerPort(KafkaCluster.EXTERNAL_PORT_NAME, KafkaCluster.EXTERNAL_PORT, "TCP")), is(true));
// Check external bootstrap service
Service ext = kc.generateExternalBootstrapService();
assertThat(ext.getMetadata().getName(), is(KafkaCluster.externalBootstrapServiceName(cluster)));
assertThat(ext.getSpec().getType(), is("NodePort"));
assertThat(ext.getSpec().getSelector(), is(kc.getSelectorLabels().toMap()));
assertThat(ext.getSpec().getPorts(), is(Collections.singletonList(kc.createServicePort(KafkaCluster.EXTERNAL_PORT_NAME, KafkaCluster.EXTERNAL_PORT, KafkaCluster.EXTERNAL_PORT, "TCP"))));
checkOwnerReference(kc.createOwnerReference(), ext);
// Check per pod services
for (int i = 0; i < replicas; i++) {
Service srv = kc.generateExternalService(i);
assertThat(srv.getMetadata().getName(), is(KafkaCluster.externalServiceName(cluster, i)));
assertThat(srv.getSpec().getType(), is("NodePort"));
assertThat(srv.getSpec().getSelector().get(Labels.KUBERNETES_STATEFULSET_POD_LABEL), is(KafkaCluster.kafkaPodName(cluster, i)));
assertThat(srv.getSpec().getPorts(), is(Collections.singletonList(kc.createServicePort(KafkaCluster.EXTERNAL_PORT_NAME, KafkaCluster.EXTERNAL_PORT, KafkaCluster.EXTERNAL_PORT, "TCP"))));
checkOwnerReference(kc.createOwnerReference(), srv);
}
}
Integration tests
Next up are the integration tests, which move one level higher.
You can find these tests inside our api module.
Moreover, there are test cases inside the topic-operator and cluster-operator modules.
Inside topic-operator you can find TopicOperatorIT, and inside cluster-operator you can take a look on the
KafkaAssemblyOperatorTest and KafkaAssemblyOperatorMockTest.
Before we get into the details again, we should understand the informal definition of integration tests.
Integration tests determine if independently developed units of software work correctly when they are connected to each other. (Martin Fowler)
In our case, the purpose of integration tests is to ensure that we can create a resource from the POJOs, serialize it and create the resource in Kubernetes. You should notice that in this level of testing we are using the Kubernetes cluster for validation of custom resources. To make this all easy to understand let me clarify it with example:
@Test
public void testKafkaWithEntityOperator() {
createDelete(Kafka.class, "Kafka-with-entity-operator.yaml");
}
protected <T extends CustomResource> void createDelete(Class<T> resourceClass, String resource) {
T model = loadResource(resourceClass, resource); // load model in that case Kafka model representation
String modelStr = TestUtils.toYamlString(model);
assertDoesNotThrow(() -> createDelete(modelStr),
"Create delete failed after first round-trip -- maybe a problem with a defaulted value?\nApplied string: " + modelStr);
}
You can see that the test case basically applies the YAML representation of Kafka and expects the creation of a Kafka customer
resource object in Kubernetes cluster is correct.
This is applied for every custom resource supported by Strimzi, such as KafkaConnect, KafkaMirrorMaker, KafkaBridge,
KafkaTopic, KafkaUser and so on.
System tests
After this level is successfully completed we can talk about our system tests. Take a look at the informal definition again:
System testing is a level of testing that validates the complete and fully integrated software product. The purpose of a system test is to evaluate the end-to-end system specifications. Usually, the software is only one element of a larger computer-based system. Ultimately, the software is interfaced with other software/hardware systems. System Testing is actually a series of different tests whose sole purpose is to exercise the full computer-based system. (Guru)
In our case the system tests validates all components and features that Strimzi provides. Everything is tested in a Kubernetes environment to fully represent a production or user environment. Again, to clarify I have an example:
@Test
void testReceiveSimpleMessage() {
// creates topic with general properties
KafkaTopicResource.topic(CLUSTER_NAME, TOPIC_NAME).done();
// deploys KafkaBridge consumer client
kafkaBridgeClientJob.consumerStrimziBridge().done();
// create instance of Kafka client with specific configuration
InternalKafkaClient internalKafkaClient = new InternalKafkaClient.Builder()
.withTopicName(TOPIC_NAME)
.withNamespaceName(NAMESPACE)
.withClusterName(CLUSTER_NAME)
.withMessageCount(MESSAGE_COUNT)
.withKafkaUsername(USER_NAME)
.withUsingPodName(kafkaClientsPodName)
.build();
// send messages to Kafka + verification
assertThat(internalKafkaClient.sendMessagesPlain(), is(MESSAGE_COUNT));
// waiting until KafkaBridge consumer client successfully end
ClientUtils.waitForClientSuccess(consumerName, NAMESPACE, MESSAGE_COUNT);
}
@BeforeAll
void createClassResources() throws Exception {
deployClusterOperator(NAMESPACE);
LOGGER.info("Deploy Kafka and KafkaBridge before tests");
// deploy kafka
KafkaResource.kafkaEphemeral(CLUSTER_NAME, 1, 1).done();
KafkaClientsResource.deployKafkaClients(false, KAFKA_CLIENTS_NAME).done();
kafkaClientsPodName = kubeClient().listPodsByPrefixInName(KAFKA_CLIENTS_NAME).get(0).getMetadata().getName();
// deploy http bridge
KafkaBridgeResource.kafkaBridge(CLUSTER_NAME, KafkaResources.plainBootstrapAddress(CLUSTER_NAME), 1)
.editSpec()
.withNewConsumer()
.addToConfig(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest")
.endConsumer()
.endSpec()
.done();
}
As you can see, this test case ensures that we are able to receive messages from the KafkaBridge component.
In the setup phase we deploy cluster-operator, a single one node Kafka cluster and also the KafkaBridge itself.
Everything is deployed into a real Kubernetes cluster.
All test cases with related test suites can be found in the systemtest
module.
Also worth mentioning is that our system tests currently take around 30 hours.
We have 3 separate sub-sets of tests, which run in parallel so it is “only” approximately 10 hours per each sub-set.
Additionally, there is a mvn profile for the main groups - acceptance, regression, smoke, bridge and all,
but we suggest to use a profile with id all (default) and then include or exclude specific groups.
If you want specify the profile, Strimzi uses maven in its build and you can run particular test groups using a maven
profile.
Here is an example of how we trigger some profiles:
Generic definition
mvn -f ${workspace}/systemtest/pom.xml -P all verify -Dgroups=${testProfile} -DexcludedGroups=${excludeGroups}
Acceptance profile
mvn -f ${workspace}/systemtest/pom.xml -P all verify -Dgroups=acceptance -DexcludedGroups=""
All available test groups are listed in the Constants class. If you’re interested, you can view our testing document.
How system tests run in relation with Kubernetes
As mentioned in the previous section, system tests run against a real Kubernetes cluster.
You can run these tests against any Kubernetes cluster from Minikube, through to a big Kubernetes or Openshift cluster.
In the test module we have a representation of a cluster that determines which environment it is running on.
Moreover, we are using Azure pipelines to trigger these tests and using Minikube.
How to run system tests
Running system tests is nothing to be scared of. In fact, it’s easy.
The steps are as follows:
- Fork the strimzi-kafka-operator
- Build the whole project in root directory -
mvn clean install -DskipTests - Set up your Kubernetes cluster
- Log in to your Kubernetes cluster
- Try to start some of our tests inside system the
testmodule, for instance -io.strimzi.systemtest.bridge.HttpBridgeST.testSendSimpleMessage()(this takes around 5 minutes)
It will run tests against the images as they are published in Dockerhub by default.
If you want to use your local images you should take a look on these environment variables:
- DOCKER_ORG = Specify the organization/repo containing the image used in system tests (default value
strimzi) - DOCKER_TAG = Specify the image tags used in system tests (default value
latest) - DOCKER_REGISTRY = Specify the docker registry used in system tests (default value
docker.io)
Using custom images
If you are interested in running custom images, you should take a look at the file that is located in the install directory: here. There are few attributes that you need to change, including:
Cluster Operatorimage = specify in local version of this fileKafkaimage =STRIMZI_KAFKA_IMAGESKafkaConnectimage =STRIMZI_KAFKA_CONNECT_IMAGESKafkaConnectS2Iimage =STRIMZI_KAFKA_CONNECT_S2I_IMAGESKafkaMirrorMakerimage =STRIMZI_KAFKA_MIRROR_MAKER_IMAGESKafkaMirrorMake2image =STRIMZI_KAFKA_MIRROR_MAKER_2_IMAGESTopic Operatorimage =STRIMZI_DEFAULT_TOPIC_OPERATOR_IMAGEUser Operatorimage =STRIMZI_DEFAULT_USER_OPERATOR_IMAGEKafkaBridgeimage =STRIMZI_DEFAULT_KAFKA_BRIDGE_IMAGECruiseControlimage =STRIMZI_DEFAULT_CRUISE_CONTROL_IMAGE
Using IDE
You can also build and run tests from your favourite IDE. In my case, I use IntelliJ and the steps are as follows.
- Checkout the strimzi-kafka-operator
- Open the strimzi-kafka-operator project
- You should probably see on the right side maven modules. Select systemtest module.
- Click on Lifecycle and then you will see
cleanandinstall - Run any test you want
Conclusion
In this post we had a quick dive into Strimzi’s system tests domain.
We learned about the motivation behind writing system tests.
And we talked about the main differences between system tests and unit and integration tests.
After, we looked at the relationship between system tests and Kubernetes.
Lastly, we showed how to start a system test.
Now you will be able to create your completely new system test in Strimzi!
Happy testing!!!
