KUBERNETES WITH JUNIT5 ON INTEGRATION TESTING

 

 Hi Folks,

With Hoverfly you can easily mock HTTP traffic during automated tests. Kubernetes is also based on the REST API. Today, I’m going to show you how to use both these tools together to improve integration testing on Kubernetes.

In the first step, we will build an application that uses the fabric8 Kubernetes Client. We don’t have to use it directly. Therefore, I’m going to include Spring Cloud Kubernetes. It uses the fabric8 client for integration with Kubernetes API. Moreover, the fabric8 client provides a mock server for the integration tests

BUILDING APPLICATIONS WITH SPRING CLOUD KUBERNETES

Spring Cloud Kubernetes provides implementations of well known Spring Cloud components based on Kubernetes API. It includes a discovery client, load balancer, and property sources support. We should add the following Maven dependency to enable it in our project.

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-kubernetes-all</artifactId>

</dependency>

Our application connects to the Mongo database, exposes REST API, and communicates with other applications over HTTP. Therefore we need to include some additional dependencies.

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-openfeign</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-mongodb</artifactId>

</dependency>

TESTING API WITH KUBERNETES MOCKSERVER

First, we need to include a Spring Boot Test starter, that contains basic dependencies used for JUnit tests implementation. Since our application is connected to Mongo and Kubernetes API, we should also mock them during the test. Here’s the full list of required dependencies.

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>de.flapdoodle.embed</groupId>

<artifactId>de.flapdoodle.embed.mongo</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>io.fabric8</groupId>

<artifactId>kubernetes-server-mock</artifactId>

<version>4.10.3</version>

<scope>test</scope>

</dependency>

Let’s discuss what exactly is happening during our test.

         (1)   First, we are enabling fabric8 Kubernetes Client JUnit5 extension in CRUD mode. It means that we                  can create a Kubernetes object on the mocked server.

(2)   Then the KubernetesClient is injected to the test by the JUnit5 extension.

(3)   TestRestTemplate is able to call endpoints exposed by the application that is started during the test.


(4)   We need to set the basic properties for KubernetesClient like a default namespace name, master URL.


        (5)   We are creating ConfigMap that contains application.properties file. ConfigMap with name                                 employee is automatically read by the application employee.

           (6)   In the test method we are using TestRestTemplate to call REST endpoints. We are mocking Kubernetes                     API and running Mongo database in the embedded mode.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @EnableKubernetesMockClient(crud = true) // (1) @TestMethodOrder(MethodOrderer.Alphanumeric.class)


class EmployeeAPITest {

static KubernetesClient client; // (2) @Autowired

TestRestTemplate restTemplate; // (3)

@BeforeAll static void init() {

System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, client.getConfiguration().getMasterUrl()); System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");

System.setProperty( Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");

System.setProperty( Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false");

System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "default"); // (4)

client.configMaps().inNamespace("default").createNew()

.withNewMetadata().withName("employee").endMetadata()

.addToData("application.properties", "spring.data.mongodb.uri=mongodb://localhost:27017/test")

.done(); // (5)

}

@Test // (6)

void addEmployeeTest() {

Employee employee = new Employee(1L, 1L, "Test", 30, "test"); employee = restTemplate.postForObject("/", employee, Employee.class); Assertions.assertNotNull(employee); Assertions.assertNotNull(employee.getId());

}

@Test

void addAndThenFindEmployeeByIdTest() {

Employee employee = new Employee(1L, 2L, "Test2", 20, "test2"); employee = restTemplate.postForObject("/", employee, Employee.class); Assertions.assertNotNull(employee); Assertions.assertNotNull(employee.getId());

employee = restTemplate

.getForObject("/{id}", Employee.class, employee.getId()); Assertions.assertNotNull(employee); Assertions.assertNotNull(employee.getId());

}


@Test

void findAllEmployeesTest() { Employee[] employees =

restTemplate.getForObject("/", Employee[].class); Assertions.assertEquals(2, employees.length);

}

@Test

void findEmployeesByDepartmentTest() { Employee[] employees =

restTemplate.getForObject("/department/1", Employee[].class); Assertions.assertEquals(1, employees.length);

}

@Test

void findEmployeesByOrganizationTest() { Employee[] employees =

restTemplate.getForObject("/organization/1", Employee[].class); Assertions.assertEquals(2, employees.length);

}

}

INTEGRATION TESTING ON KUBERNETES WITH HOVERFLY

To test HTTP communication between applications we usually need to use an additional tool for mocking API. Hoverfly is an ideal solution for such a use case. It is a lightweight, open-source API simulation tool not only for REST-based applications. It allows you to write tests in Java and Python. In addition, it also supports JUnit5. You need to include the following dependencies to enable it in your project.



<dependency>

<groupId>io.specto</groupId>

<artifactId>hoverfly-java-junit5</artifactId>

<version>0.13.0</version>

<scope>test</scope>

</dependency>

<dependency>

<groupId>io.specto</groupId>

<artifactId>hoverfly-java</artifactId>

<version>0.13.0</version>

<scope>test</scope>

</dependency>

You can enable Hoverfly in your tests with @ExtendWith annotation. It automatically starts Hoverfly proxy during a test. Our main goal is to mock the Kubernetes client. To do that we still need to set some properties inside @BeforeAll method. The default URL used by KubernetesClient is kubernetes.default.svc. In the first step, we are mocking configmap endpoint and returning predefined Kubernetes ConfigMap with application.properties.

The name of ConfigMap is the same as the application name. We are testing communication from the department application to the employee application.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ExtendWith(HoverflyExtension.class)

public class DepartmentAPIAdvancedTest {

@Autowired KubernetesClient client;

@BeforeAll

static void setup(Hoverfly hoverfly) { System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false");

System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "default");


hoverfly.simulate(dsl(service("kubernetes.default.svc")

.get("/api/v1/namespaces/default/configmaps/department")

.willReturn(success().body(json(buildConfigMap())))));

}

private static ConfigMap buildConfigMap() {

return new ConfigMapBuilder().withNewMetadata()

.withName("department").withNamespace("default")

.endMetadata()

.addToData("application.properties", "spring.data.mongodb.uri=mongodb://localhost:27017/test")

.build();

}

// TESTS ...

}

After application startup, we may use TestRestTemplate to call a test endpoint. The endpoint GET

/organization/{organizationId}/with-employees retrieves data from the employee application. It finds the department by organization id and then finds all employees assigned to the department. We need to mock a target endpoint using Hoverfly. But before that, we are mocking Kubernetes APIs responsible for getting service and endpoint by name. The address and port returned by the mocked endpoints must be the same as the address of a target application endpoint.

@Autowired

TestRestTemplate restTemplate;

private final String EMPLOYEE_URL = "employee.default:8080"; @Test

void findByOrganizationWithEmployees(Hoverfly hoverfly) { Department department = new Department(1L, "Test");

department = restTemplate.postForObject("/", department, Department.class); Assertions.assertNotNull(department); Assertions.assertNotNull(department.getId());

hoverfly.simulate( dsl(service(prepareUrl())

.get("/api/v1/namespaces/default/endpoints/employee")

.willReturn(success().body(json(buildEndpoints())))), dsl(service(prepareUrl())

.get("/api/v1/namespaces/default/services/employee")

.willReturn(success().body(json(buildService())))), dsl(service(EMPLOYEE_URL)

.get("/department/" + department.getId())

.willReturn(success().body(json(buildEmployees())))));

Department[] departments = restTemplate

.getForObject("/organization/{organizationId}/with-employees", Department[].class, 1L); Assertions.assertEquals(1, departments.length);

Assertions.assertEquals(1, departments[0].getEmployees().size());

}

private Service buildService() {

return new ServiceBuilder().withNewMetadata().withName("employee")

.withNamespace("default").withLabels(new HashMap<>())

.withAnnotations(new HashMap<>()).endMetadata().withNewSpec().addNewPort()

.withPort(8080).endPort().endSpec().build();

}

private Endpoints buildEndpoints() {

return new EndpointsBuilder().withNewMetadata()

.withName("employee").withNamespace("default")

.endMetadata()


.addNewSubset().addNewAddress()

.withIp("employee.default").endAddress().addNewPort().withName("http")

.withPort(8080).endPort().endSubset()

.build();

}

private List<Employee> buildEmployees() { List<Employee> employees = new ArrayList<>(); Employee employee = new Employee(); employee.setId("abc123"); employee.setAge(30); employee.setName("Test"); employee.setPosition("test"); employees.add(employee);

return employees;

}

private String prepareUrl() {

return client.getConfiguration().getMasterUrl()

.replace("/", "")

.replace("https:", "");



}


Post a Comment

3 Comments