Testing Java Apps on Kubernetes with Testkube
In this article, you will learn how to test Java apps on Kubernetes with Testkube automatically. We will build the tests for the typical Spring REST-based app. In the first scenario, Testkube runs the JUnit tests using its Maven support. After that, we will run the load tests against the running instance of our app using the Grafana k6 tool. Once again, Kubetest provides a standard mechanism for that, no matter which tool we use for testing.
If you are interested in testing on Kubernetes you can also read my article about integration tests with JUnit. There is also a post about contract testing on Kubernetes with Microcks available here.
Introduction
Testkube is a Kubernetes native test orchestration and execution framework. It allows us to run automated tests inside the Kubernetes cluster. It supports several popular testing or build tools like JMeter, Grafana k6, and Maven. We can easily integrate with the CI/CD pipelines or GitOps workflows. We can manage Kubetest by using the CRD objects directly, with the CLI, or through the UI dashboard. Let’s check how it works.
Source Code
If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. It contains only a single app. Once you clone it you can go to the src/test
directory. You will find there both the JUnit tests written in Java and the k6 tests written in JavaScript. After that, you should just follow my instructions. Let’s begin.
Run Kubetest on Kubernetes
In the first step, we are going to install Testkube on Kubernetes using its Helm chart. Let’s add the kubeshop
Helm repository and fetch latest charts info:
$ helm repo add kubeshop https://kubeshop.github.io/helm-charts
$ helm repo update
Then, we can install Testkube in the testkube
namespace by executing the following helm
command:
$ helm install testkube kubeshop/testkube \
--create-namespace --namespace testkube
This will add custom resource definitions (CRD), RBAC roles, and role bindings to the Kubernetes cluster. This installation requires having cluster administrative rights.
Once the installation is finished, we can verify a list of running in the testkube
namespace. The testkube-api-server
and testkube-dashboard
are the most important components. However, there are also some additional tools installed like Mongo database or Minio.
$ oc get po -n testkube
NAME READY STATUS RESTARTS AGE
testkube-api-server-d4d7f9f8b-xpxc9 1/1 Running 1 (6h17m ago) 6h18m
testkube-dashboard-64578877c7-xghsz 1/1 Running 0 6h18m
testkube-minio-testkube-586877d8dd-8pmmj 1/1 Running 0 6h18m
testkube-mongodb-dfd8c7878-wzkbp 1/1 Running 0 6h18m
testkube-nats-0 3/3 Running 0 6h18m
testkube-nats-box-567d94459d-6gc4d 1/1 Running 0 6h18m
testkube-operator-controller-manager-679b998f58-2sv2x 2/2 Running 0 6h18m
We can also install testkube
CLI on our laptop. It is not required, but we will use it during the exercise just try the full spectrum of options. You can find CLI installation instructions here. I’m installing it on macOS:
$ brew install testkube
Once the installation is finished, you can run the testkube version
command to see that warm “Hello” screen 🙂
Run Maven Tests with Testkube
Firstly, let’s take a look at the JUnit tests inside our sample Spring Boot app. We are using the TestRestTemplate
bean to call all the exposed REST endpoints exposed. There are three JUnit tests for testing adding, getting, and removing the Person
objects.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class PersonControllerTests {
@Autowired
lateinit var template: TestRestTemplate
@Test
@Order(1)
fun shouldAddPerson() {
var person = Instancio.of(Person::class.java)
.ignore(Select.field("id"))
.create()
person = template
.postForObject("/persons", person, Person::class.java)
Assertions.assertNotNull(person)
Assertions.assertNotNull(person.id)
Assertions.assertEquals(1001, person.id)
}
@Test
@Order(2)
fun shouldUpdatePerson() {
var person = Instancio.of(Person::class.java)
.set(Select.field("id"), 1)
.create()
template.put("/persons", person)
var personRemote = template
.getForObject("/persons/{id}", Person::class.java, 1)
Assertions.assertNotNull(personRemote)
Assertions.assertEquals(person.age, personRemote.age)
}
@Test
@Order(3)
fun shouldDeletePerson() {
template.delete("/persons/{id}", 1)
val person = template
.getForObject("/persons/{id}", Person::class.java, 1)
Assertions.assertNull(person)
}
}
We are using Maven as a build tool. The current version of Spring Boot is 3.2.0
. The version of JDK used for the compilation is 17. Here’s the fragment of our pom.xml
in the repository root directory:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>pl.piomin.services</groupId>
<artifactId>sample-spring-kotlin-microservice</artifactId>
<version>1.5.3</version>
<properties>
<java.version>17</java.version>
<kotlin.version>1.9.21</kotlin.version>
</properties>
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.instancio</groupId>
<artifactId>instancio-junit</artifactId>
<version>3.6.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Testkube provides the Executor
CRD for defining a way of running each test. There are several default executors per each type of supported build or test tool. We can display a list of provided executors by running the testkube get executor
command. You will see the list of all tools supported by Testkube. Of course, the most interesting executors for us are k6-executor
and maven-executor
.
$ testkube get executor
Context: (1.16.8) Namespace: testkube
----------------------------------------
NAME | URI | LABELS
-----------------------+-----+-----------------------------------
artillery-executor | |
curl-executor | |
cypress-executor | |
ginkgo-executor | |
gradle-executor | |
jmeter-executor | |
jmeterd-executor | |
k6-executor | |
kubepug-executor | |
maven-executor | |
playwright-executor | |
postman-executor | |
soapui-executor | |
tracetest-executor | |
zap-executor | |
By default, maven-executor
uses JDK 11 for running Maven tests. Moreover, it still doesn’t provide images for running tests against JDK19+. For me, this is quite a big drawback since the latest LTS version of Java is 21. The maven-executor-jdk17
Executor
contains the name of the running image (1) and a list of supported test types (2).
apiVersion: executor.testkube.io/v1
kind: Executor
metadata:
name: maven-executor-jdk17
namespace: testkube
spec:
args:
- '--settings'
- <settingsFile>
- <goalName>
- '-Duser.home'
- <mavenHome>
command:
- mvn
content_types:
- git-dir
- git
executor_type: job
features:
- artifacts
# (1)
image: kubeshop/testkube-maven-executor:jdk17
meta:
docsURI: https://kubeshop.github.io/testkube/test-types/executor-maven
iconURI: maven
# (2)
types:
- maven:jdk17/project
- maven:jdk17/test
- maven:jdk17/integration-test
Finally, we just need to define the Test
object that references to maven-executor-jdk17
by the type
parameter. Of course, we also need to set the address of the Git repository and the name of the branch.
apiVersion: tests.testkube.io/v3
kind: Test
metadata:
name: sample-spring-kotlin
namespace: testkube
spec:
content:
repository:
branch: master
type: git
uri: https://github.com/piomin/sample-spring-kotlin-microservice.git
type: git
type: maven:jdk17/test
Finally, we can run the sample-spring-kotlin
test using the following command:
$ testkube run test sample-spring-kotlin
Using UI Dashboard
First of all, let’s expose the Testkube UI dashboard on the local port. The dashboard also requires a connection to the testkube-api-server
from the web browser. After exposing the dashboard with the following port-forward
command we can access it under the http://localhost:8080
address:
$ kubectl port-forward svc/testkube-dashboard 8080 -n testkube
$ kubectl port-forward svc/testkube-api-server 8088 -n testkube
Once we access the Testkube dashboard we will see a list of all defined tests:
Then, we can click the selected tile with the test to see the details. You will be redirected to the history of previous executions available in the “Recent executions” tab. There are six previous executions of our sample-spring-kotlin
test. Two of them were finished successfully, the four others were failed.
Let’s take a look at the logs of the last one execution. As you see, all three JUnit tests were successful.
Run Load Tests with Testkube and Grafana k6
In this section, we will create the tests for the instance of our sample app running on Kubernetes. So, in the first step, we need to deploy the app. Here’s the Deployment
manifest. We can apply it to the default
namespace. The manifest uses the latest image of the sample app available in the registry under the quay.io/pminkows/sample-kotlin-string:1.5.3
address.
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-kotlin-spring
labels:
app: sample-kotlin-spring
spec:
replicas: 1
selector:
matchLabels:
app: sample-kotlin-spring
template:
metadata:
labels:
app: sample-kotlin-spring
spec:
containers:
- name: sample-kotlin-spring
image: quay.io/pminkows/sample-kotlin-spring:1.5.3
ports:
- containerPort: 8080
Let’s also create the Kubernetes Service
that exposes app pods internally:
apiVersion: v1
kind: Service
metadata:
name: sample-kotlin-spring
spec:
selector:
app: sample-kotlin-spring
ports:
- protocol: TCP
port: 8080
targetPort: 8080
After that, we can proceed to the Test
manifest. This time, we don’t have to override the default executor, since the k6
version is not important. The test source is located inside the sample Git repository in the src/test/resources/k6/load-tests-get.js
(1) file in the master
branch. In that case, the repository type is git
(2). The k6
test should run for 5 seconds and should use 5 concurrent threads (3). We also need to set the address of a target service as the PERSONS_URI
environment variable (4). Of course, we are testing through the Kubernetes Service visible internally under the sample-kotlin-spring.default.svc
host and port 8080
. The type of the test is k6/script
(5).
apiVersion: tests.testkube.io/v3
kind: Test
metadata:
labels:
executor: k6-executor
test-type: k6-script
name: load-tests-gets
namespace: testkube
spec:
content:
repository:
branch: master
# (1)
path: src/test/resources/k6/load-tests-get.js
# (2)
type: git
uri: https://github.com/piomin/sample-spring-kotlin-microservice.git
type: git
executionRequest:
# (3)
args:
- '-u'
- '5'
- '-d'
- 10s
# (4)
variables:
PERSONS_URI:
name: PERSONS_URI
type: basic
value: http://sample-kotlin-spring.default.svc:8080
valueFrom: {}
# (5)
type: k6/script
Let’s take a look at the k6
test file written in JavaScript. As I mentioned before, you can find it in the src/test/resources/k6/load-tests-get.js
file. The test calls the GET /persons/{id}
endpoint. It sets the random number between 1
and 1000
as the id
path parameter and reads a target service URL from the PERSONS_URI
environment variable.
import http from 'k6/http';
import { check } from 'k6';
import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
export default function () {
const id = randomIntBetween(1, 1000);
const res = http.get(`${__ENV.PERSONS_URI}/persons/${id}`);
check(res, {
'is status 200': (res) => res.status === 200,
'body size is > 0': (r) => r.body.length > 0,
});
}
Finally, we can run the load-tests-gets
test with the following command:
$ testkube run test load-tests-gets
The same as for the Maven test we can verify the execution history in the Testkube dashboard:
We can also display all the logs from the test:
Final Thoughts
Testkube provides a unified way to run Kubernetes tests for the several most popular testing tools. It may be a part of your CI/CD pipeline or a GitOps process. Honestly, I’m still not very convinced if I need a dedicated Kubernetes-native solution for automated tests, instead e.g. a stage in my pipeline that runs test commands. However, you can also use Testkube to execute load or integration tests against the app running on Kubernetes. It is possible to schedule them periodically. Thanks to that you can verify your apps continuously using a single, central tool.
Leave a Reply