Structural testing, also known as white-box testing, is a testing methodology that is based on the internal paths, structure, and implementation of the software. Unlike black-box testing, which focuses on the external functionality and user interface, structural testing dives into the "white" or transparent box, which is the code itself.
Structural testing is critical for several reasons:
-
Comprehensive Coverage: It ensures that all the possible paths within the codebase are executed at least once. This is crucial for uncovering hidden bugs that may not be immediately apparent through functional testing.
-
Logic Verification: By examining the logical flow of the application, structural testing verifies the correctness of the code logic, which is vital for the application to behave as expected.
-
Early Detection of Defects: Since structural testing involves testing the code during the development phase, defects can be detected early, reducing the cost and effort required to fix them later.
-
Optimization Opportunities: It provides insights into the performance and optimization opportunities for the code, as testers can see which parts of the code are executed frequently and which are not.
-
Security Assessments: Structural testing can uncover security vulnerabilities within the code, such as buffer overflows or logic flaws that could be exploited.
When performing structural testing, several activities are typically involved:
- Code Coverage Analysis: This involves using tools to assess which parts of the code are executed during testing. It measures various types of coverage, like line, branch, and method coverage.
- Test Case Design: Based on the coverage analysis, test cases are designed to target untested parts of the code to improve the coverage.
- Test Execution and Monitoring: The designed test cases are executed, and their execution is monitored to ensure they are covering the intended parts of the code.
- Results Analysis: After testing, the results are analyzed to determine the effectiveness of the test cases and to identify areas for further improvement.
Consider a simple Java method that calculates the factorial of a number:
public int factorial(int number) {
int result = 1;
for (int factor = 2; factor <= number; factor++) {
result *= factor;
}
return result;
}
A structural test case for this method would not only test it with different input values (like a black-box test) but also ensure that the loop inside the method is executed the correct number of times for each input. For instance, input 5 should cause the loop to execute five times, multiplying the result by 2, 3, 4, and 5.
To have structural testing, the tools we use usually, include JaCoCo, Clover, or Cobertura. Here we choose JaCoCo.
To use Jacoco
coverage tool in a maven project, we could add the following dependency in each "pom.xml" (We have already done so to our project).
<build>
<plugins>
<!-- ... other plugins ... -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version> <!-- Use the latest version available -->
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- ... other plugins ... -->
</plugins>
</build>
- To run tests with Jacoco and generate a report in html format, run the following command under folder "kafka-ui":
mvn clean test jacoco:report
- You could find the report "index.html" under path "kafka-ui-api/target/site/jacoco". Open "index.html" in a web browser to read the report.
- If you use Linux System as a non-root user, please remember to add "sudo" before each command.
- If you get stuck while downloading
jacoco-maven-plugin
, try running:
rm -rf ~/.m2/repository/org/jacoco/jacoco-maven-plugin/0.8.7/
Then run:
mvn clean test jacoco:report
Original Report is under folder "jacoco-reports/report-20240217".
The following table is an overview of the test coverage of the project.
Element | Missed Instructions | Cov. | Missed Branches | Cov. | Missed Cxty | Missed Lines | Missed Methods | Missed Classes |
---|---|---|---|---|---|---|---|---|
Total | 47,036 of 82,073 | 42% | 5,218 of 6,534 | 20% | 5,933 | 8,548 | 8,288 | 14,678 |
ksql | 16,182 of 5,497 | 25% | 1,192 of 72 | 5% | 2,053 | 2,134 | 4,130 | 4,596 |
com.provectus.kafka.ui.model | 7,087 of 2,449 | 25% | 1,215 of 39 | 3% | 906 | 1,188 | 196 | 547 |
com.provectus.kafka.ui.controller | 4,642 of 2,031 | 30% | 287 of 53 | 15% | 363 | 509 | 1,200 | 1,649 |
com.provectus.kafka.ui.config | 2,941 of 751 | 20% | 625 of 31 | 4% | 427 | 564 | 51 | 198 |
com.provectus.kafka.ui.service | 2,443 of 5,399 | 68% | 181 of 122 | 40% | 292 | 768 | 383 | 1,423 |
com.provectus.kafka.ui.config.auth | 1,912 | 5% | 255 | 0% | 259 | 269 | 217 | 235 |
com.provectus.kafka.ui.service.rbac.extractor | 1,110 | 0% | 42 | 0% | 105 | 105 | 261 | 261 |
com.provectus.kafka.ui.model.rbac | 1,097 of 406 | 27% | 239 | 2% | 169 | 208 | 61 | 141 |
com.provectus.kafka.ui.util | 1,018 of 1,025 | 50% | 104 of 60 | 36% | 127 | 226 | 184 | 416 |
com.provectus.kafka.ui.mapper | 922 of 1,376 | 59% | 144 of 92 | 38% | 153 | 239 | 286 | 666 |
com.provectus.kafka.ui.service.rbac | 905 | 10% | 119 | 4% | 120 | 131 | 225 | 256 |
com.provectus.kafka.ui.service.metrics | 732 of 761 | 50% | 93 of 59 | 38% | 96 | 159 | 148 | 305 |
com.provectus.kafka.ui.serdes | 722 of 1,188 | 62% | 81 of 73 | 47% | 85 | 185 | 128 | 366 |
com.provectus.kafka.ui.util.jsonschema | 717 of 2,430 | 77% | 112 of 177 | 61% | 97 | 293 | 75 | 544 |
com.provectus.kafka.ui.service.integration.odd | 714 of 715 | 50% | 53 of 17 | 24% | 78 | 136 | 124 | 281 |
com.provectus.kafka.ui.service.ksql | 698 of 587 | 45% | 11 of 14 | 11% | 97 | 147 | 52 | 171 |
com.provectus.kafka.ui.model.schemaregistry | 431 | 0% | 76 | 0% | 77 | 77 | 16 | 16 |
com.provectus.kafka.ui.serdes.builtin | 330 of 2,433 | 88% | 26 of 6 | 63% | 66 | 228 | 87 | 428 |
com.provectus.kafka.ui.serdes.builtin.sr | 284 of 771 | 73% | 21 of 5 | 54% | 28 | 78 | 42 | 219 |
com.provectus.kafka.ui.service.ksql.response | 280 of 218 | 43% | 36 of 5 | 40% | 36 | 59 | 69 | 128 |
com.provectus.kafka.ui.service.integration.odd.schema | 258 of 1,221 | 82% | 58 of 139 | 70% | 58 | 150 | 41 | 343 |
com.provectus.kafka.ui.exception | 241 of 512 | 67% | - | 66% | 35 | 79 | 76 | 200 |
com.provectus.kafka.ui.model.connect | 190 | 25% | 38 | 0% | 25 | 37 | 1 | 6 |
com.provectus.kafka.ui.service.analyze | 91 of 0 | 84% | 22 of 6 | 54% | 28 | 79 | 31 | 187 |
com.provectus.kafka.ui.config.auth.logout | - | 0% | - | n/a | 9 | 9 | 38 | 38 |
com.provectus.kafka.ui.emitter | 1,610 | 91% | 228 | 79% | 28 | 164 | 36 | 389 |
com.provectus.kafka.ui.service.audit | 736 | 84% | 134 | 79% | 19 | 80 | 32 | 189 |
com.provectus.kafka.ui.service.masking | 193 | 59% | 32 of 18 | 36% | 24 | 42 | 4 | 42 |
com.provectus.kafka.ui.client | - | 58% | - | n/a | 21 | 44 | 22 | 65 |
com.provectus.kafka.ui.service.acl | 661 | 86% | 28 | 73% | 19 | 59 | 20 | 173 |
com.provectus.kafka.ui.model.rbac.permission | 226 | 73% | - | n/a | 14 | 27 | 18 | 63 |
com.provectus.kafka.ui.service.masking.policies | 484 | 88% | 43 | 84% | 9 | 62 | 11 | 111 |
com.provectus.kafka.ui.model.rbac.provider | - | 0% | - | n/a | 4 | 4 | 13 | 13 |
com.provectus.kafka.ui | - | 11% | - | n/a | 2 | 3 | 6 | 7 |
- Instructions Coverage: 42% (47,036 of 82,073 instructions missed)
- Branch Coverage: 20% (5,218 of 6,534 branches missed)
- Line Coverage: 2,978 of 14,678 lines missed (20.3%)
- Method Coverage: 207 of 5,113 methods missed (4.04%)
1. KafkaUiApplication Class
Element | Missed Instructions | Cov. | Missed Branches | Cov. | Missed | Cxty | Missed Lines | Missed Methods |
---|---|---|---|---|---|---|---|---|
Total | 24 of 27 | 11% | 0 of 0 | n/a | 2 | 3 | 6 | 7 |
startApplication(String[]) | 20 | 0% | n/a | n/a | 1 | 1 | 4 | 4 |
main(String[]) | 4 | 0% | n/a | n/a | 1 | 1 | 2 | 2 |
KafkaUiApplication() | 3 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
0% coverage indicates that the methods are not tested
2. ConsumingStats
Element | Missed Instructions | Cov. | Missed Branches | Cov. | Missed | Cxty | Missed Lines | Missed Methods |
---|---|---|---|---|---|---|---|---|
Total | 7 of 92 | 92% | 0 of 0 | n/a | 1 | 5 | 2 | 24 |
incFilterApplyError() | 7 | 0% | n/a | n/a | 1 | 1 | 2 | 1 |
sendConsumingEvt(FluxSink, PolledRecords) | 35 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
createConsumingStats() | 23 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
ConsumingStats() | 15 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
sendFinishEvent(FluxSink) | 12 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
3. RetryingKafkaConnectClient
Element | Missed Instructions | Cov. | Missed Branches | Cov. | Missed | Cxty | Missed Lines | Missed Methods |
---|---|---|---|---|---|---|---|---|
Total | 114 of 231 | 50% | 0 of 0 | n/a | 21 | 42 | 22 | 51 |
restartConnector(String, Boolean, Boolean) | 7 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
restartConnectorWithHttpInfo(String, Boolean, Boolean) | 7 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
lambda$conflictCodeRetry$1(RetryBackoffSpec, Retry.RetrySignal) | 7 | 0% | n/a | n/a | 1 | 1 | 2 | 1 |
getConnectorTaskStatusWithHttpInfo(String, Integer) | 6 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
restartConnectorTask(String, Integer) | 6 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
restartConnectorTaskWithHttpInfo(String, Integer) | 6 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
setConnectorConfigWithHttpInfo(String, Map) | 6 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
validateConnectorPluginConfigWithHttpInfo(String, Map) | 6 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
createConnectorWithHttpInfo(NewConnector) | 5 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
deleteConnectorWithHttpInfo(String) | 5 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
getConnectorWithHttpInfo(String) | 5 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
getConnectorConfigWithHttpInfo(String) | 5 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
getConnectorStatusWithHttpInfo(String) | 5 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
getConnectorTasksWithHttpInfo(String) | 5 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
getConnectorTopicsWithHttpInfo(String) | 5 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
getConnectorsWithHttpInfo(String) | 5 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
pauseConnector(String) | 5 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
pauseConnectorWithHttpInfo(String) | 5 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
resumeConnectorWithHttpInfo(String) | 5 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
getConnectorPluginsWithHttpInfo() | 4 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
resumeConnector(String) | 4 | 0% | n/a | n/a | 1 | 1 | 1 | 1 |
RetryingKafkaConnectClient(ClustersProperties.ConnectCluster, ClustersProperties.TruststoreConfig, DataSize) | 9 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
conflictCodeRetry() | 8 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
withBadRequestErrorHandling(Mono) | 8 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
static {...} | 7 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
setConnectorConfig(String, Map) | 6 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
getConnectorTaskStatus(String, Integer) | 6 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
validateConnectorPluginConfig(String, Map) | 6 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
lambda$withBadRequestErrorHandling$3(WebClientResponseException.InternalServerError) | 6 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
lambda$withBadRequestErrorHandling$2(WebClientResponseException.BadRequest) | 6 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
createConnector(NewConnector) | 5 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
deleteConnector(String) | 5 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
getConnector(String) | 5 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
getConnectorConfig(String) | 5 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
getConnectorStatus(String) | 5 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
getConnectorTasks(String) | 5 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
getConnectorTopics(String) | 5 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
getConnectors(String) | 5 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
withRetryOnConflict(Mono) | 4 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
withRetryOnConflict(Flux) | 4 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
getConnectorPlugins() | 4 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
lambda$conflictCodeRetry$0(Throwable) | 3 | 100% | n/a | n/a | 0 | 1 | 0 | 1 |
1. On KafkaUiApplication
class
public class KafkaUiApplication {
public static void main(String[] args) {
startApplication(args);
}
public static ConfigurableApplicationContext startApplication(String[] args) {
return new SpringApplicationBuilder(KafkaUiApplication.class)
.initializers(DynamicConfigOperations.dynamicConfigPropertiesInitializer())
.build()
.run(args);
}
}
2. On ConsumingStats
class
void incFilterApplyError() {
filterApplyErrors++;
}
void sendFinishEvent(FluxSink<TopicMessageEventDTO> sink) {
sink.next(
new TopicMessageEventDTO()
.type(TopicMessageEventDTO.TypeEnum.DONE)
.consuming(createConsumingStats())
);
}
3. On RetryingKafkaConnectClient
class
@Override
public Mono<Void> restartConnector(String connectorName, Boolean includeTasks, Boolean onlyFailed)
throws WebClientResponseException {
return withRetryOnConflict(super.restartConnector(connectorName, includeTasks, onlyFailed));
}
@Override
public Mono<ResponseEntity<Void>> restartConnectorWithHttpInfo(String connectorName, Boolean includeTasks,
Boolean onlyFailed) throws WebClientResponseException {
return withRetryOnConflict(super.restartConnectorWithHttpInfo(connectorName, includeTasks, onlyFailed));
}
@Override
public Mono<Void> restartConnectorTask(String connectorName, Integer taskId) throws WebClientResponseException {
return withRetryOnConflict(super.restartConnectorTask(connectorName, taskId));
}
@Override
public Mono<ResponseEntity<Void>> restartConnectorTaskWithHttpInfo(String connectorName, Integer taskId)
throws WebClientResponseException {
return withRetryOnConflict(super.restartConnectorTaskWithHttpInfo(connectorName, taskId));
}
We added new test cases for three Test classes:
kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaTopicCreateTests.java
kafka-ui-api/src/test/java/com/provectus/kafka/ui/controller/ApplicationConfigControllerTest.java
kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/ksql/response/ResponseParserTest.java
Through these new test cases, we reduced total missed lines of code from 8288 to 8236.
New coverage report could be found at "jacoco-reports/report-20240218".
Here is the comparison on the final line between the old report and new report.
Old Report
Class | Instructions Missed | Coverage | Lines Missed |
---|---|---|---|
Total | 47,036 of 82,073 | 42% | 8,288 of 14,678 |
New Report
Class | Instructions Missed | Coverage | Lines Missed |
---|---|---|---|
Total | 46,822 of 82,073 | 42% | 8,236 of 14,678 |
We added these new test cases:
/*
* Checks if a 400 status code is returned when an invalid replication factor is provided.
*/
@Test
void shouldReturn400IfInvalidReplicationFactor() {
TopicCreationDTO topicCreation = new TopicCreationDTO()
.replicationFactor(0) // Invalid replication factor
.partitions(3)
.name(UUID.randomUUID().toString());
webTestClient.post()
.uri("/api/clusters/{clusterName}/topics", LOCAL)
.bodyValue(topicCreation)
.exchange()
.expectStatus()
.isBadRequest();
}
/*
* Checks if a 400 status code is returned when an invalid number of partitions is provided
*/
@Test
void shouldReturn400IfInvalidPartitions() {
TopicCreationDTO topicCreation = new TopicCreationDTO()
.replicationFactor(1)
.partitions(0) // Invalid number of partitions
.name(UUID.randomUUID().toString());
webTestClient.post()
.uri("/api/clusters/{clusterName}/topics", LOCAL)
.bodyValue(topicCreation)
.exchange()
.expectStatus()
.isBadRequest();
}
These test cases mainly improve coverage of class "GlobalErrorWebExceptionHandler".
Coverage report before new test cases
Element | Missed Instructions | Cov. | Missed Lines |
---|---|---|---|
Total | 103 of 299 | 65% | 24 |
render(ResponseStatusException, ServerRequest) | 48 | 0% | 11 |
renderDefault(Throwable, ServerRequest) | 41 | 0% | 10 |
renderErrorResponse(ServerRequest) | 1125 | 69% | 2 |
render(WebExchangeBindException, ServerRequest) | 352 | 94% | 1 |
render(CustomBaseException, ServerRequest) | 44 | 100% | 11 |
extractFieldErrorMsg(FieldError) | 20 | 100% | 1 |
lambda$render$1(Map.Entry) | 17 | 100% | 4 |
GlobalErrorWebExceptionHandler(ErrorAttributes...) | 12 | 100% | 3 |
coalesce(Object[]) | 8 | 100% | 1 |
getRoutingFunction(ErrorAttributes) | 5 | 100% | 1 |
requestId(ServerRequest) | 5 | 100% | 1 |
lambda$render$0(FieldError) | 5 | 100% | 1 |
currentTimestamp() | 3 | 100% | 1 |
Coverage report after new test cases
Element | Missed Instructions | Cov. | Missed Branches | Cov. | Missed | Cxty | Missed Lines | Missed Methods | Missed Classes |
---|---|---|---|---|---|---|---|---|---|
Total | 103 of 299 | 65% | 3 of 8 | 62% | 5 | 17 | 24 | 72 | 2 |
render(WebExchangeBindException, ServerRequest) | 352 | 94% | 11 | 50% | 1 | 2 | 1 | 20 | 0 |
render(ResponseStatusException, ServerRequest) | 48 | 100% | n/a | 0 | 1 | 0 | 11 | 0 | 1 |
render(CustomBaseException, ServerRequest) | 44 | 100% | n/a | 0 | 1 | 0 | 11 | 0 | 1 |
renderDefault(Throwable, ServerRequest) | 41 | 100% | n/a | 0 | 1 | 0 | 10 | 0 | 1 |
renderErrorResponse(ServerRequest) | 36 | 100% | 6 | 100% | 0 | 4 | 0 | 8 | 0 |
extractFieldErrorMsg(FieldError) | 20 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
lambda$render$1(Map.Entry) | 17 | 100% | n/a | 0 | 1 | 0 | 4 | 0 | 1 |
GlobalErrorWebExceptionHandler(ErrorAttributes...) | 12 | 100% | n/a | 0 | 1 | 0 | 3 | 0 | 1 |
coalesce(Object[]) | 8 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
getRoutingFunction(ErrorAttributes) | 5 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
requestId(ServerRequest) | 5 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
lambda$render$0(FieldError) | 5 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
currentTimestamp() | 3 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
New test cases:
// Verifies that the parseSelectHeadersString method correctly parses headers with different data types.
@Test
void parsesSelectHeaderWithDifferentDataTypes() {
assertThat(ResponseParser.parseSelectHeadersString("col1 INT, col2 STRING, col3 DOUBLE"))
.containsExactly("col1 INT", "col2 STRING", "col3 DOUBLE");
assertThat(ResponseParser.parseSelectHeadersString("name STRING, age INT, active BOOLEAN"))
.containsExactly("name STRING", "age INT", "active BOOLEAN");
}
// Verifies that the parseSelectHeadersString method correctly parses headers with different nested structures.
@Test
void parsesSelectHeaderWithDifferentStructures() {
assertThat(ResponseParser.parseSelectHeadersString(
"`struct1` STRUCT<`nested1` STRING, `nested2` INT>, `struct2` STRUCT<`nested3` DOUBLE>"))
.containsExactly(
"`struct1` STRUCT<`nested1` STRING, `nested2` INT>",
"`struct2` STRUCT<`nested3` DOUBLE>"
);
assertThat(ResponseParser.parseSelectHeadersString(
"`parent` STRUCT<`child1` INT, `child2` STRING>, `nested` STRUCT<`inner1` DOUBLE>"))
.containsExactly(
"`parent` STRUCT<`child1` INT, `child2` STRING>",
"`nested` STRUCT<`inner1` DOUBLE>"
);
}
// Verifies that the parseSelectResponse method correctly parses and returns a response table for schema headers.
@Test
void parseSelectResponseWithSchemaHeader() {
// Prepare JSON node with "header" field for schema
ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
jsonNode.set("header", JsonNodeFactory.instance.objectNode().put("schema", "col1 INT, col2 STRING"));
// Invoke the method
Optional<KsqlApiClient.KsqlResponseTable> responseTable = ResponseParser.parseSelectResponse(jsonNode);
// Verify the result
assertThat(responseTable).isPresent();
assertThat(responseTable.get().getHeader()).isEqualTo("Schema");
assertThat(responseTable.get().getColumnNames()).containsExactly("col1 INT", "col2 STRING");
assertThat(responseTable.get().getValues()).isNull();
}
// Verifies that the parseSelectResponse method throws a NullPointerException when encountering row data (possibly indicating that row data is not expected in this context).
@Test
void parseSelectResponseWithRowData() {
// Prepare JSON node with "row" field for row data
ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
jsonNode.set("row", JsonNodeFactory.instance.objectNode().putArray("columns").add("value1").add("value2"));
// Invoke the method
assertThatThrownBy(() -> ResponseParser.parseSelectResponse(jsonNode))
.isInstanceOf(NullPointerException.class);
}
// Verifies that the errorTableWithTextMsg method correctly creates an error response table with the provided error text.
@Test
void errorTableWithTextMsg() {
// Prepare test data
String errorText = "An error occurred!";
// Invoke the method
KsqlApiClient.KsqlResponseTable responseTable = ResponseParser.errorTableWithTextMsg(errorText);
// Verify the result
assertThat(responseTable.getHeader()).isEqualTo("Execution error");
assertThat(responseTable.getColumnNames()).containsExactly("message");
assertThat(responseTable.getValues()).containsExactly(List.of(new TextNode(errorText)));
assertThat(responseTable.isError()).isTrue();
}
// Verifies that the parseErrorResponse method correctly parses and returns an error response table.
@Test
void parseErrorResponse() {
String errorMessage = "An error occurred!";
ObjectNode jsonNode = new ObjectMapper().createObjectNode();
jsonNode.put("errorMessage", errorMessage);
WebClientResponseException webClientResponseException = WebClientResponseException
.create(500, "Internal Server Error", null, null, null);
var responseTable = ResponseParser.parseErrorResponse(webClientResponseException);
assertThat(responseTable.getHeader()).isEqualTo("Execution error");
assertThat(responseTable.isError()).isTrue();
}
These test cases mainly improve coverage of class "com.provectus.kafka.ui.service.ksql.response".
Coverage report before new test cases:
Class | Instructions Missed | Coverage | Branches Missed | Coverage | Complexity (Cxty) |
---|---|---|---|---|---|
Total | 280 of 498 | 43% | 36 of 61 | 40% | - |
ResponseParser | 258137 of 391075 | 34% | 3324 of 7884 | 42% | 49 |
DynamicParser | 2281 of 10488 | 78% | 31 of 124 | 25% | 10 |
Coverage report after new test cases:
Class | Instructions Missed | Coverage | Branches Missed | Coverage | Complexity (Cxty) |
---|---|---|---|---|---|
Total | 193 of 498 | 61% | 29 of 61 | 52% | - |
ResponseParser | 186209 of 352938 | 52% | 2730 of 5656 | 52% | 49 |
DynamicParser | 796 of 12216 | 93% | 22 of 44 | 50% | 10 |
New test cases:
// Check the behavior when attempting to upload a file that doesn't exist.
@Test
public void testUploadWithInvalidFile() {
var invalidFile = new ClassPathResource("/invalidFile.txt", this.getClass());
webTestClient.post()
.uri("/api/config/relatedfiles")
.bodyValue(generateBody(invalidFile))
.exchange()
.expectStatus().is5xxServerError();
}
// Check the behavior when attempting to upload a file to a non-existent endpoint.
@Test
public void testUploadEndpointNotFound() {
var fileToUpload = new ClassPathResource("/fileForUploadTest.txt", this.getClass());
webTestClient.post()
.uri("/api/config/nonexistentendpoint")
.bodyValue(generateBody(fileToUpload))
.exchange()
.expectStatus().isNotFound();
}
These test cases mainly improve coverage of class "GlobalErrorWebExceptionHandler".
Coverage report before new test cases
Element | Missed Instructions | Cov. | Missed Branches | Cov. | Missed | Cxty | Missed Lines | Missed Methods | Missed Classes |
---|---|---|---|---|---|---|---|---|---|
Total | 103 of 299 | 65% | 3 of 8 | 62% | 5 | 17 | 24 | 72 | 2 |
render(ResponseStatusException, ServerRequest) | 48 | 0% | n/a | 1 | 1 | 11 | 11 | 1 | 1 |
renderDefault(Throwable, ServerRequest) | 41 | 0% | n/a | 1 | 1 | 10 | 10 | 1 | 1 |
renderErrorResponse(ServerRequest) | 1125 | 69% | 24 | 66% | 2 | 4 | 2 | 8 | 0 |
render(WebExchangeBindException, ServerRequest) | 352 | 94% | 11 | 50% | 1 | 2 | 1 | 20 | 0 |
render(CustomBaseException, ServerRequest) | 44 | 100% | n/a | 0 | 1 | 0 | 11 | 0 | 1 |
extractFieldErrorMsg(FieldError) | 20 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
lambda$render$1(Map.Entry) | 17 | 100% | n/a | 0 | 1 | 0 | 4 | 0 | 1 |
GlobalErrorWebExceptionHandler(ErrorAttributes...) | 12 | 100% | n/a | 0 | 1 | 0 | 3 | 0 | 1 |
coalesce(Object[]) | 8 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
getRoutingFunction(ErrorAttributes) | 5 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
requestId(ServerRequest) | 5 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
lambda$render$0(FieldError) | 5 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
currentTimestamp() | 3 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
Coverage report after new test cases
Element | Missed Instructions | Cov. | Missed Branches | Cov. | Missed | Cxty | Missed Lines | Missed Methods | Missed Classes |
---|---|---|---|---|---|---|---|---|---|
Total | 3 of 299 | 98% | 1 of 8 | 87% | 1 | 17 | 1 | 72 | 0 |
render(WebExchangeBindException, ServerRequest) | 352 | 94% | 11 | 50% | 1 | 2 | 1 | 20 | 0 |
render(ResponseStatusException, ServerRequest) | 48 | 100% | n/a | 0 | 1 | 0 | 11 | 0 | 1 |
render(CustomBaseException, ServerRequest) | 44 | 100% | n/a | 0 | 1 | 0 | 11 | 0 | 1 |
renderDefault(Throwable, ServerRequest) | 41 | 100% | n/a | 0 | 1 | 0 | 10 | 0 | 1 |
renderErrorResponse(ServerRequest) | 36 | 100% | 6 | 100% | 0 | 4 | 0 | 8 | 0 |
extractFieldErrorMsg(FieldError) | 20 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
lambda$render$1(Map.Entry) | 17 | 100% | n/a | 0 | 1 | 0 | 4 | 0 | 1 |
GlobalErrorWebExceptionHandler(ErrorAttributes...) | 12 | 100% | n/a | 0 | 1 | 0 | 3 | 0 | 1 |
coalesce(Object[]) | 8 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
getRoutingFunction(ErrorAttributes) | 5 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
requestId(ServerRequest) | 5 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
lambda$render$0(FieldError) | 5 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
currentTimestamp() | 3 | 100% | n/a | 0 | 1 | 0 | 1 | 0 | 1 |
[1] Unstop. (n.d.). Unstop - Competitions, Quizzes, Hackathons, Scholarships and Internships for Students and Corporates. https://unstop.com/blog/structural-testing
[2] What is White Box Testing?. (n.d.). Check Point. https://www.checkpoint.com/cyber-hub/cyber-security/what-is-white-box-testing/#:~:text=White%20box%20testing%20is%20a,gray%20and%20black%20box%20testing
[3] White box testing - Software engineering- GeeksforGeeks. (2023, December 27). GeeksforGeeks. https://www.geeksforgeeks.org/software-engineering-white-box-testing/