Effective Kubernetes Testing in Java with Fabric8 Kube API Test

(Posted by Rohan Kumar on 29/06/2024 tagged kubernetes java envtest testing )

Introduction:

Writing tests is extermely important in any kind of application, whether it’s a normal application or a Kubernetes application. But how do you test your code that’s written to interact with Kubernetes API Server? Fabric8 Kubernetes Client offers Fabric8 Kubernetes Mock Server that creates a mock server instance which can mimic a Kubernetes API Server which is quite handy for testing simple scenarios. I have written these blog posts about it in case you’re interested:

While Fabric8 Kubernetes Mock Server works for simple to slightly complex scenarios, users usually start failing issues as it is not the same as a real Kubernetes API Server. When you start using advanced features of Kubernetes with Fabric8 Kubernetes Mock Server , you will start facing issues as it would not return the same respone as expected from a real Kubernetes API Server.

Last year Fabric8 Kube API Test was contributed to Fabric8 Kubernetes Client project by Attila Mészáros. We are going to talk about this module in this blogpost and see how it improves testing support for Kubernetes Java applications by creating a ephemeral test instance of Kubernetes API Server for testing.

History of the Project

Fabric8 Kube API Test was initially created as a PoC by Attila Mészáros. It was inspired by GoLang Controller RunTime Library envtest. It was initially called jenvtest but while migrating to Fabric8 Kubernetes Client, it was renamed to Kube API Test.

Prerequisites:

You would need the following things to be able to follow this article:

Setting up Application:

You can either use an existing project or create a new one. I would be using my existing Kubernetes Client Demo repository for writing test using Fabric8 Kube API Test.

Open the pom.xml and add Fabric8 Kube API Test in <dependency> section:

<dependency>
    <groupId>io.fabric8</groupId>
    <artifactId>kube-api-test</artifactId>
    <version>${fabric8.version}</version>
    <scope>test</scope>
</dependency>

Now we should be able to use Fabric8 Kube API Test in our project.

Using Fabric8 Kube API Test in your project

Using Fabric8 Kube API Test is just about adding this annotation in your test class:

@EnableKubeAPIServer // Enable Kube Api Test Module in the test
class PodGroupServiceTest {
  // ...
}

This is going to download and run Kubernetes API server binaries directly in the background. It would download the binaries in ~/.kubeapitest directory.

In order to interact with Kubernetes API server binaries running in the background, we would need to create KubernetesClient instance. Fabric8 Kube API Test provides @KubeConfig annotation to inject kube config yaml to initialize any type of client. We are going to use Fabric8 Kubernetes Client in this blogpost. Here is sample code of initializing KubernetesClient that would interact with this test api server.

@EnableKubeAPIServer
class PodGroupServiceTest {
  KubernetesClient kubernetesClient;

  @KubeConfig
  static String configYaml;

  @BeforeEach
  void setUp() {
    kubernetesClient = new KubernetesClientBuilder()
      .withConfig(Config.fromKubeconfig(configYaml))
      .build();
  }

  // ...
}

Now let’s write a test for some class using Fabric8 Kube API Test. We will be using existing test class PodGroupService that we used in How to write tests using Fabric8 blog post. It’s a very simple class that groups a list of pods by a set of labels and performs operations on that group.

Let’s say we want to write test for PodGroupService’s addToGroup method. Here is how we will do it using Fabric8 Kube API Test :

@EnableKubeAPIServer(kubeAPIVersion = "1.30.0")
class PodGroupServiceTest {
  KubernetesClient kubernetesClient;

  @KubeConfig
  static String configYaml;

  @BeforeEach
  void setUp() {
    kubernetesClient = new KubernetesClientBuilder()
      .withConfig(Config.fromKubeconfig(configYaml))
      .build();
  }

  @Test
  void addToGroup_whenPodProvided_thenShouldUpdatePod() {
    // Given
    Map<String, String> matchLabel = Collections.singletonMap("app", "add-to-group");
    PodGroupService podGroupService = new PodGroupService(kubernetesClient, matchLabel);
    Pod p1 = createNewPod("p1", "add-to-group");

    // When
    podGroupService.addToGroup(p1);

    // Then
    PodList podList = podGroupService.list();
    assertTrue(podList.getItems().stream().map(Pod::getMetadata).map(ObjectMeta::getName).anyMatch(n -> n.startsWith("p1")));
  }

  private Pod createNewPod(String generateName, String appLabelValue) {
    return new PodBuilder()
      .withNewMetadata().withGenerateName(generateName).withLabels(Collections.singletonMap("app", appLabelValue)).endMetadata()
      .withNewSpec()
      .addNewContainer()
      .withName("demo-container")
      .withImage("alpine:latest")
      .endContainer()
      .endSpec()
      .build();
  }
}

You can see that experience of using Fabric8 Kube API Test is as transparent as using a real Kubernetes cluster. There is no need to add any kind of expectations as we did in case of Fabric8 Kubernetes Mock Server.

Similarly, you can also write tests for more involved use cases like watch. Here is an example (I’ve omitted other details as they’re same in all examples):

  @Test
  void watch_whenInvoked_shouldMonitorUpdates() throws Exception {
    // Given
    PodGroupService podGroupService = new PodGroupService(kubernetesClient, Collections.singletonMap("app", "watch-test"));
    CountDownLatch eventReceivedLatch = new CountDownLatch(1);

    // When
    try (Watch ignore = podGroupService.watch(new Watcher<>() {
      @Override
      public void eventReceived(Action action, Pod pod) {
        eventReceivedLatch.countDown();
      }

      @Override
      public void onClose(WatcherException e) { }
    })) {
      podGroupService.addToGroup(createNewPod("p1-watch", "watch-test"));
      assertTrue(eventReceivedLatch.await(5, TimeUnit.SECONDS));
    }
    // Then
    assertEquals(0, eventReceivedLatch.getCount());
  }

Once initialized properly, writing tests using Fabric8 Kube API Test as just like writing tests against a real Kubernetes Api server.

Fabric8 Kube API Test Configuration Options:

Fabric8 Kube API Test provides these configuration options that can be provided to @EnableKubeAPIServer annotation. Here is an example of @EnableKubeAPIServer configured with available configuration options:

@EnableKubeAPIServer(
  // Kubernetes Api Server version
  kubeAPIVersion = "1.30.0",
  // Kubernetes Api Server Flags
  apiServerFlags =  {"--audit-webhook-truncate-enabled"},
  // Whether modify local kube config
  updateKubeConfigFile = false
)

Conclusion:

In this blog post, you learned how you can leverage on Fabric8 Kube API Test to write robust tests that can ensure that your application is behaving as expected.

You can find code related to this blog post in this GitHub Repository.

To learn more about Fabric8 Kubernetes Client, check these links: