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>
</dependency>
(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:",
"");
}
3 Comments
please make a note on docker
ReplyDeleteThanks for the post keep sharing
ReplyDeleteNice post thanks for sharing
ReplyDelete