Skip to content

Commit

Permalink
Merge pull request #14821 from FroMage/14744
Browse files Browse the repository at this point in the history
Support KubernetesServer test resource for CRUD operations
  • Loading branch information
FroMage authored Feb 19, 2021
2 parents f5337f5 + 35d76c4 commit fb082bd
Show file tree
Hide file tree
Showing 24 changed files with 809 additions and 94 deletions.
57 changes: 57 additions & 0 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -987,11 +987,68 @@ A very common need is to start some services on which your Quarkus application d
By simply annotating any test in the test suite with `@QuarkusTestResource`, Quarkus will run the corresponding `QuarkusTestResourceLifecycleManager` before any tests are run.
A test suite is also free to utilize multiple `@QuarkusTestResource` annotations, in which case all the corresponding `QuarkusTestResourceLifecycleManager` objects will be run before the tests. When using multiple test resources they can be started concurrently. For that you need to set `@QuarkusTestResource(parallel = true)`.

NOTE: test resources are global, even if they are defined on a test class or custom profile, which means they will all be activated for all tests, even though we do
remove duplicates. If you want to only enable a test resource on a single test class or test profile, you can use `@QuarkusTestResource(restrictToAnnotatedClass = true)`.

Quarkus provides a few implementations of `QuarkusTestResourceLifecycleManager` out of the box (see `io.quarkus.test.h2.H2DatabaseTestResource` which starts an H2 database, or `io.quarkus.test.kubernetes.client.KubernetesMockServerTestResource` which starts a mock Kubernetes API server),
but it is common to create custom implementations to address specific application needs.
Common cases include starting docker containers using https://www.testcontainers.org/[Testcontainers] (an example of which can be found https://github.com/quarkusio/quarkus-quickstarts/blob/master/kafka-quickstart/src/test/java/org/acme/kafka/KafkaResource.java[here]),
or starting a mock HTTP server using http://wiremock.org/[Wiremock] (an example of which can be found https://github.com/geoand/quarkus-test-demo/blob/master/src/test/java/org/acme/getting/started/country/WiremockCountries.java[here]).

=== Annotation-based test resources

It is possible to write test resources that are enabled and configured using annotations. This is enabled by placing the `@QuarkusTestResource`
on an annotation which will be used to enable and configure the test resource.

For example, this defines the `@WithKubernetesTestServer` annotation, which you can use on your tests to activate the `KubernetesServerTestResource`,
but only for the annotated test class. You can also place them on your `QuarkusTestProfile` test profiles.

[source,java]
----
@QuarkusTestResource(KubernetesServerTestResource.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WithKubernetesTestServer {
/**
* Start it with HTTPS
*/
boolean https() default false;
/**
* Start it in CRUD mode
*/
boolean crud() default true;
/**
* Port to use, defaults to any available port
*/
int port() default 0;
}
----

The `KubernetesServerTestResource` class has to implement the
`QuarkusTestResourceConfigurableLifecycleManager` interface in order to be configured using the previous annotation:

[source,java]
----
public class KubernetesServerTestResource
implements QuarkusTestResourceConfigurableLifecycleManager<WithKubernetesTestServer> {
private boolean https = false;
private boolean crud = true;
private int port = 0;
@Override
public void init(WithKubernetesTestServer annotation) {
this.https = annotation.https();
this.crud = annotation.crud();
this.port = annotation.port();
}
// ...
}
----

== Hang Detection

`@QuarkusTest` has support for hang detection to help diagnose any unexpected hangs. If no progress is made for a specified
Expand Down
17 changes: 9 additions & 8 deletions docs/src/main/asciidoc/kubernetes-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ public class KubernetesClientProducer {

== Testing

To make testing against a mock Kubernetes API extremely simple, Quarkus provides the `KubernetesMockServerTestResource` which automatically launches
To make testing against a mock Kubernetes API extremely simple, Quarkus provides the `WithKubernetesTestServer` annotation which automatically launches
a mock of the Kubernetes API server and sets the proper environment variables needed so that the Kubernetes Client configures itself to use said mock.
Tests can inject the mock and set it up in any way necessary for the particular testing using the `@MockServer` annotation.
Tests can inject the mock server and set it up in any way necessary for the particular testing using the `@KubernetesTestServer` annotation.

Let's assume we have a REST endpoint defined like so:

Expand All @@ -100,12 +100,13 @@ We could write a test for this endpoint very easily like so:

[source%nowrap,java]
----
@QuarkusTestResource(KubernetesMockServerTestResource.class)
// you can even configure aspects like crud, https and port on this annotation
@WithKubernetesTestServer
@QuarkusTest
public class KubernetesClientTest {
@MockServer
KubernetesMockServer mockServer;
@KubernetesTestServer
KubernetesServer mockServer;
@BeforeEach
public void before() {
Expand Down Expand Up @@ -139,14 +140,14 @@ Note that to take advantage of these features, the `quarkus-test-kubernetes-clie
</dependency>
----

You can create a `CustomKubernetesMockServerTestResource.java` to ensure all your `@QuarkusTest` enabled test classes share the same mock server setup:
Alternately, you can create a `CustomKubernetesMockServerTestResource.java` to ensure all your `@QuarkusTest` enabled test classes share the same mock server setup:

[source%nowrap,java]
----
public class CustomKubernetesMockServerTestResource extends KubernetesMockServerTestResource {
public class CustomKubernetesMockServerTestResource extends KubernetesServerTestResource {
@Override
public void configureMockServer(KubernetesMockServer mockServer) {
public void configureMockServer(KubernetesServer mockServer) {
mockServer.expect().get().withPath("/api/v1/namespaces/test/pods")
.andReturn(200, new PodList())
.always();
Expand Down
3 changes: 3 additions & 0 deletions integration-tests/kubernetes-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
</configuration>
<executions>
<!--
The prod mode tests need to be part of a different execution to ensure that they don't mess with the standard tests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public Response updateFirst(@PathParam("namespace") String namespace) {
final Pod pod = pods.get(0);
final String podName = pod.getMetadata().getName();
// would normally do some kind of meaningful update here
Pod updatedPod = new PodBuilder().withNewMetadata().withName(podName).withNewResourceVersion("12345").endMetadata()
Pod updatedPod = new PodBuilder().withNewMetadata().withName(podName).withNewResourceVersion("12345")
.addToLabels("key1", "value1").endMetadata()
.build();

updatedPod = kubernetesClient.pods().withName(podName).createOrReplace(updatedPod);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import io.quarkus.test.QuarkusProdModeTest;
import io.quarkus.test.common.QuarkusTestResource;

@QuarkusTestResource(CustomKubernetesMockServerTestResource.class)
@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedClass = true)
public class AbsentConfigMapPropertiesPMT {

@RegisterExtension
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTestResource(CustomKubernetesMockServerTestResource.class)
@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedClass = true)
@QuarkusTest
public class ConfigMapPropertiesTest {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.quarkus.it.kubernetes.client;

import java.util.Base64;

import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.quarkus.test.kubernetes.client.KubernetesServerTestResource;

public class CustomKubernetesTestServerTestResource extends KubernetesServerTestResource {

// setup the ConfigMap objects that the application expects to lookup configuration from
@Override
protected void configureServer() {
server.getClient().inNamespace("test").configMaps().create(configMapBuilder("cmap1")
.addToData("dummy", "dummy")
.addToData("overridden.secret", "cm") // will be overridden since secrets have a higher priority
.addToData("some.prop1", "val1")
.addToData("some.prop2", "val2")
.addToData("some.prop4", "v4") // will be overridden since cmap2 has a higher priority
.addToData("some.prop5", "val5")
.addToData("application.properties", "some.prop3=val3")
.addToData("application.yaml", "some:\n prop4: val4").build());

server.getClient().inNamespace("test").configMaps().create(configMapBuilder("cmap2")
.addToData("application.yaml", "some:\n prop4: val4").build());

server.getClient().inNamespace("test").configMaps().create(configMapBuilder("cmap3")
.addToData("dummy", "dummyFromDemo")
.addToData("some.prop1", "val1FromDemo")
.addToData("some.prop2", "val2FromDemo")
.addToData("some.prop5", "val5FromDemo")
.addToData("application.properties", "some.prop3=val3FromDemo")
.addToData("application.yaml", "some:\n prop4: val4FromDemo").build());

server.getClient().inNamespace("test").secrets().create(secretBuilder("s1")
.addToData("dummysecret", encodeValue("dummysecret"))
.addToData("overridden.secret", encodeValue("secret"))
.addToData("secret.prop1", encodeValue("val1"))
.addToData("secret.prop2", encodeValue("val2"))
.addToData("application.properties", encodeValue("secret.prop3=val3"))
.addToData("application.yaml", encodeValue("secret:\n prop4: val4")).build());

server.getClient().inNamespace("test").secrets().create(secretBuilder("s1")
.addToData("dummysecret", encodeValue("dummysecretFromDemo"))
.addToData("overridden.secret", encodeValue("secretFromDemo"))
.addToData("secret.prop1", encodeValue("val1FromDemo"))
.addToData("secret.prop2", encodeValue("val2FromDemo"))
.addToData("application.properties", encodeValue("secret.prop3=val3FromDemo"))
.addToData("application.yaml", encodeValue("secret:\n prop4: val4FromDemo")).build());
}

private ConfigMapBuilder configMapBuilder(String name) {
return new ConfigMapBuilder().withNewMetadata()
.withName(name).endMetadata();
}

private SecretBuilder secretBuilder(String name) {
return new SecretBuilder().withNewMetadata()
.withName(name).endMetadata();
}

private String encodeValue(String value) {
return Base64.getEncoder().encodeToString(value.getBytes());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* KubernetesClientTest.TestResource contains the entire process of setting up the Mock Kubernetes API Server
* It has to live there otherwise the Kubernetes client in native mode won't be able to locate the mock API Server
*/
@QuarkusTestResource(CustomKubernetesMockServerTestResource.class)
@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedClass = true)
@QuarkusTest
public class KubernetesClientTest {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.quarkus.it.kubernetes.client;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;

import org.junit.jupiter.api.Test;

import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodBuilder;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.kubernetes.client.KubernetesTestServer;
import io.restassured.RestAssured;

/*
* KubernetesClientTest.TestResource contains the entire process of setting up the Mock Kubernetes API Server
* It has to live there otherwise the Kubernetes client in native mode won't be able to locate the mock API Server
*/
@QuarkusTestResource(value = CustomKubernetesTestServerTestResource.class, restrictToAnnotatedClass = true)
@QuarkusTest
public class KubernetesNewClientTest {

@KubernetesTestServer
private KubernetesServer mockServer;

@Test
public void testInteractionWithAPIServer() throws InterruptedException {
setupMockServerForTest();

RestAssured.when().get("/pod/test").then()
.body("size()", is(2)).body(containsString("pod1"), containsString("pod2"));

RestAssured.when().delete("/pod/test").then()
.statusCode(204);

RestAssured.when().put("/pod/test").then()
.body(containsString("value1"));

RestAssured.when().post("/pod/test").then()
.body(containsString("12345"));
}

private void setupMockServerForTest() {
Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build();
Pod pod2 = new PodBuilder().withNewMetadata().withName("pod2").withNamespace("test").and().build();

mockServer.getClient().inNamespace("test").pods().create(pod1);
mockServer.getClient().inNamespace("test").pods().create(pod2);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.quarkus.it.kubernetes.client;

import io.quarkus.test.junit.NativeImageTest;

@NativeImageTest
public class KubernetesNewClientTestIT extends KubernetesNewClientTest {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.quarkus.it.kubernetes.client;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import io.quarkus.test.kubernetes.client.KubernetesTestServer;
import io.quarkus.test.kubernetes.client.WithKubernetesTestServer;

/*
* This class has no native-image test because it relies on setting config overrides that clash
* with native image build config.
* This is the same test as KubernetesTestServerTest but with the test resource annotation on the profile
*/
@TestProfile(KubernetesTestServerOnProfileTest.MyProfile.class)
@QuarkusTest
public class KubernetesTestServerOnProfileTest {

private static KubernetesServer setupServer;

public static class Setup implements Consumer<KubernetesServer> {

@Override
public void accept(KubernetesServer t) {
setupServer = t;
}

}

@KubernetesTestServer
private KubernetesServer mockServer;

@Test
public void testConfiguration() throws InterruptedException {
// we can't really test CRUD, and HTTPS doesn't work
Assertions.assertEquals(10001, mockServer.getMockServer().getPort());
Assertions.assertSame(mockServer, setupServer);
}

@WithKubernetesTestServer(https = false, crud = true, port = 10001, setup = KubernetesTestServerOnProfileTest.Setup.class)
public static class MyProfile implements QuarkusTestProfile {

@Override
public Map<String, String> getConfigOverrides() {
Map<String, String> overrides = new HashMap<>();
// do not fetch config from kubernetes
overrides.put("quarkus.kubernetes-config.enabled", "false");
overrides.put("quarkus.kubernetes-config.secrets.enabled", "false");
// get rid of errors due to us not populating config from kubernetes
overrides.put("dummy", "asd");
overrides.put("some.prop1", "asd");
overrides.put("some.prop2", "asd");
overrides.put("some.prop3", "asd");
overrides.put("some.prop4", "asd");
overrides.put("some.prop5", "asd");
overrides.put("secret.prop1", "asd");
overrides.put("secret.prop2", "asd");
overrides.put("secret.prop3", "asd");
overrides.put("secret.prop4", "asd");
overrides.put("overridden.secret", "asd");
overrides.put("dummysecret", "asd");
return overrides;
}
}
}
Loading

0 comments on commit fb082bd

Please sign in to comment.