Skip to content

Commit b5039cf

Browse files
Kafka integration test along with general tests improvements
1 parent f7bccf9 commit b5039cf

10 files changed

Lines changed: 322 additions & 93 deletions

memex/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@
132132
<artifactId>junit-jupiter</artifactId>
133133
<scope>test</scope>
134134
</dependency>
135+
<dependency>
136+
<groupId>org.springframework.kafka</groupId>
137+
<artifactId>spring-kafka-test</artifactId>
138+
<scope>test</scope>
139+
</dependency>
135140
<dependency>
136141
<groupId>org.testcontainers</groupId>
137142
<artifactId>mongodb</artifactId>

memex/src/main/java/com/johnlpage/memex/kafka/VehicleInspectionKafkaConsumer.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import com.johnlpage.memex.service.generic.PostWriteTriggerService;
1010
import com.johnlpage.memex.service.generic.PreWriteTriggerService;
1111
import com.mongodb.bulk.BulkWriteResult;
12-
import jakarta.annotation.PreDestroy;
1312
import java.util.ArrayList;
1413
import java.util.List;
1514
import java.util.concurrent.CompletableFuture;
@@ -18,6 +17,8 @@
1817
import lombok.RequiredArgsConstructor;
1918
import org.slf4j.LoggerFactory;
2019
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
20+
import org.springframework.context.event.ContextClosedEvent;
21+
import org.springframework.context.event.EventListener;
2122
import org.springframework.kafka.annotation.KafkaListener;
2223
import org.springframework.scheduling.annotation.Scheduled;
2324
import org.springframework.stereotype.Component;
@@ -100,8 +101,8 @@ public void checkForIdle() {
100101
}
101102
}
102103

103-
@PreDestroy
104-
public void onShutdown() {
104+
@EventListener
105+
public void onContextClosed(ContextClosedEvent event) {
105106
sendBatch();
106107
System.out.println("Kafka Listener is shutting down.");
107108
CompletableFuture<Void> allFutures =

memex/src/test/java/com/johnlpage/memex/cucumber/CucumberTestsContainersConfig.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,32 @@
33
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
44
import org.springframework.boot.test.context.TestConfiguration;
55
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Condition;
7+
import org.springframework.context.annotation.ConditionContext;
8+
import org.springframework.context.annotation.Conditional;
9+
import org.springframework.core.type.AnnotatedTypeMetadata;
610
import org.springframework.test.context.DynamicPropertyRegistrar;
711
import org.testcontainers.mongodb.MongoDBAtlasLocalContainer;
812

13+
class MongoUriMissingCondition implements Condition {
14+
@Override
15+
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
16+
String uri = context.getEnvironment().getProperty("spring.data.mongodb.uri");
17+
return uri == null || uri.trim().isEmpty();
18+
}
19+
}
20+
921
@TestConfiguration
1022
public class CucumberTestsContainersConfig {
1123

1224
@Bean
13-
@ConditionalOnProperty(name = "memex.testcontainers.mongodb.enabled", havingValue = "true", matchIfMissing = true)
25+
@Conditional(MongoUriMissingCondition.class)
1426
public MongoDBAtlasLocalContainer mongoDbContainer() {
1527
return new MongoDBAtlasLocalContainer("mongodb/mongodb-atlas-local:8");
1628
}
1729

1830
@Bean
19-
@ConditionalOnProperty(name = "memex.testcontainers.mongodb.enabled", havingValue = "true", matchIfMissing = true)
31+
@Conditional(MongoUriMissingCondition.class)
2032
public DynamicPropertyRegistrar mongoDbProperties(MongoDBAtlasLocalContainer mongoDBContainer) {
2133
mongoDBContainer.start();
2234
return (registry) -> {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.johnlpage.memex.cucumber.steps;
2+
3+
import com.johnlpage.memex.cucumber.CucumberTestsContainersConfig;
4+
import io.cucumber.java.Before;
5+
import io.cucumber.spring.CucumberContextConfiguration;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.boot.test.context.SpringBootTest;
9+
import org.springframework.data.mongodb.core.MongoTemplate;
10+
import org.springframework.data.mongodb.core.query.Criteria;
11+
import org.springframework.data.mongodb.core.query.Query;
12+
import org.springframework.kafka.test.context.EmbeddedKafka;
13+
import org.springframework.test.context.ActiveProfiles;
14+
import org.springframework.test.context.TestPropertySource;
15+
16+
@CucumberContextConfiguration
17+
@SpringBootTest(classes = CucumberTestsContainersConfig.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
18+
@ActiveProfiles("test")
19+
@EmbeddedKafka(partitions = 1, topics = {"test"})
20+
public class CucumberSpringContextConfig {
21+
22+
}

memex/src/test/java/com/johnlpage/memex/cucumber/steps/InspectionSteps.java

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,47 @@
66
import static io.restassured.RestAssured.given;
77
import static org.junit.jupiter.api.Assertions.*;
88

9-
import com.johnlpage.memex.cucumber.CucumberTestsContainersConfig;
9+
import com.fasterxml.jackson.core.JsonProcessingException;
10+
import com.fasterxml.jackson.databind.ObjectMapper;
11+
import com.johnlpage.memex.model.VehicleInspection;
1012
import io.cucumber.datatable.DataTable;
1113
import io.cucumber.java.ParameterType;
1214
import io.cucumber.java.en.Given;
1315
import io.cucumber.java.en.When;
1416
import io.cucumber.java.en.Then;
15-
import io.cucumber.spring.CucumberContextConfiguration;
1617
import io.restassured.http.ContentType;
1718
import io.restassured.path.json.JsonPath;
1819
import io.restassured.response.Response;
20+
1921
import java.time.ZonedDateTime;
2022
import java.time.format.DateTimeFormatter;
2123
import java.util.List;
2224
import java.util.Map;
2325
import java.util.concurrent.TimeUnit;
2426

25-
import org.springframework.boot.test.context.SpringBootTest;
27+
import org.bson.Document;
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.beans.factory.annotation.Value;
2630
import org.springframework.boot.test.web.server.LocalServerPort;
27-
import org.springframework.test.context.ActiveProfiles;
31+
import org.springframework.data.mongodb.core.MongoTemplate;
32+
import org.springframework.data.mongodb.core.query.Criteria;
33+
import org.springframework.data.mongodb.core.query.Query;
34+
import org.springframework.data.mongodb.core.query.Update;
2835

29-
@CucumberContextConfiguration
30-
@SpringBootTest(classes = CucumberTestsContainersConfig.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
31-
@ActiveProfiles("test")
3236
public class InspectionSteps {
3337

3438
@LocalServerPort
3539
private int port;
3640

41+
@Autowired
42+
private MongoTemplate mongoTemplate;
43+
44+
@Autowired
45+
private ObjectMapper objectMapper;
46+
47+
@Autowired
48+
private VehicleInspectionIdRangeValidator idRangeValidator;
49+
3750
private Response response;
3851
private ZonedDateTime capturedTimestamp;
3952

@@ -42,23 +55,72 @@ public String baseUrl() {
4255
}
4356

4457
@ParameterType("true|false")
45-
public Boolean bool(String bool){
58+
public Boolean bool(String bool) {
4659
return Boolean.parseBoolean(bool);
4760
}
4861

4962
@Given("the following vehicle inspections exist:")
50-
public void givenVehicleInspectionsExist(DataTable dataTable) {
51-
// test assumes it exists however it would be better to create it here
63+
public void givenVehicleInspectionsExist(DataTable dataTable) throws JsonProcessingException {
64+
List<Map<String, String>> rows = dataTable.asMaps(String.class, String.class);
65+
66+
for (Map<String, String> row : rows) {
67+
String json = row.get("vehicleInspection");
68+
VehicleInspection inspection = objectMapper.readValue(json, VehicleInspection.class);
69+
Long testId = inspection.getTestid();
70+
assertNotNull(testId, "testid is expected to be part of the data input");
71+
72+
idRangeValidator.validate(testId);
73+
Query query = Query.query(Criteria.where("_id").is(testId));
74+
75+
Document updateDoc = new Document(objectMapper.convertValue(inspection, Map.class));
76+
Update update = Update.fromDocument(updateDoc);
77+
78+
mongoTemplate.upsert(query, update, VehicleInspection.class);
79+
80+
}
5281
}
5382

54-
@Given("the following vehicle inspections exist and have historical data as of {string}:")
55-
public void givenVehicleInspectionsExist(String date, DataTable dataTable) {
56-
// test assumes it exists however it would be better to create it here
83+
@Given("the vehicle inspections in range {long}-{long} do not exist")
84+
public void theFollowingVehicleInspectionsInRangeDoesNotExist(long startId, long endId) {
85+
idRangeValidator.validateRange(startId, endId);
86+
Query query = new Query();
87+
query.addCriteria(Criteria.where("_id").gte(startId).lte(endId));
88+
mongoTemplate.remove(query, VehicleInspection.class);
89+
}
90+
91+
@Given("the vehicle inspection with id {long} does not exist")
92+
public void theFollowingVehicleInspectionDoesNotExist(long testId) {
93+
idRangeValidator.validate(testId);
94+
Query query = new Query();
95+
query.addCriteria(Criteria.where("_id").is(testId));
96+
mongoTemplate.remove(query, VehicleInspection.class);
5797
}
5898

5999
@Given("the following vehicle inspections do not exist:")
60-
public void givenVehicleInspectionsDoNotExist(DataTable dataTable) {
61-
// test assumes it does not exist however it would be better to delete it here
100+
public void givenTheFollowingVehicleInspectionsDoNotExist(DataTable dataTable) {
101+
for (Map<String, String> row : dataTable.asMaps()) {
102+
if (row.size() != 1) {
103+
throw new IllegalArgumentException("Only one column per row is supported in this step.");
104+
}
105+
106+
String key = row.keySet().iterator().next();
107+
String value = row.get(key);
108+
109+
long rangeStart = idRangeValidator.getRangeStart();
110+
long rangeEnd = idRangeValidator.getRangeEnd();
111+
112+
Query query = Query.query(Criteria.where("_id").gte(rangeStart).lte(rangeEnd));
113+
114+
if (key.equalsIgnoreCase("testid")) {
115+
long testid = Long.parseLong(value);
116+
idRangeValidator.validate(testid);
117+
query = Query.query(Criteria.where("_id").is(testid));
118+
} else {
119+
query.addCriteria(Criteria.where(key).is(value));
120+
}
121+
mongoTemplate.remove(query, VehicleInspection.class);
122+
123+
}
62124
}
63125

64126
@Given("I capture the current timestamp")
@@ -156,7 +218,7 @@ public void eachItemInTheResponseArrayShouldContain(String key, String value) {
156218
List<Map<String, Object>> list = jsonPath.getList("$");
157219
assertFalse(list.isEmpty(), "Response array should not be empty for this check");
158220
for (Map<String, Object> item : list) {
159-
if(key.indexOf('.') > 0) {
221+
if (key.indexOf('.') > 0) {
160222
// If the value is a vehicle field, we need to check the nested structure
161223
String[] parts = key.split("\\.");
162224
assertThat(item, hasKey(parts[0]));
@@ -192,12 +254,12 @@ public void theResponseShouldBeAStreamOfValidJsonObjectsEachOnANewLine() {
192254
assertFalse(body.isEmpty(), "Response body should not be empty for stream check");
193255

194256
// Split by any standard newline character(s)
195-
String[] lines = body.split("\\r?\\n");
257+
String[] lines = body.split("\\r?\\n");
196258
assertTrue(lines.length > 0, "Response body should contain at least one line for stream check");
197259

198260
for (String line : lines) {
199261
// Skip potentially empty lines that might result from splitting (e.g., trailing newline)
200-
if (line.trim().isEmpty()) continue;
262+
if (line.trim().isEmpty()) continue;
201263
try {
202264
JsonPath.from(line); // This will throw an exception if the line is not valid JSON
203265
} catch (Exception e) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.johnlpage.memex.cucumber.steps;
2+
3+
import lombok.Getter;
4+
import org.springframework.beans.factory.annotation.Value;
5+
import org.springframework.stereotype.Component;
6+
7+
@Getter
8+
@Component
9+
public class VehicleInspectionIdRangeValidator {
10+
11+
@Value("${memex.test.data.vehicleinspection-testid-range.start:10000}")
12+
private long rangeStart;
13+
14+
@Value("${memex.test.data.vehicleinspection-testid-range.end:20000}")
15+
private long rangeEnd;
16+
17+
public void validate(long id) {
18+
if (id < rangeStart || id > rangeEnd) {
19+
throw new AssertionError("Vehicle inspection testid: " + id + " outside of specified test range");
20+
}
21+
}
22+
23+
public void validateRange(long startId, long endId) {
24+
if (endId < startId) {
25+
throw new AssertionError("Vehicle inspection end testid: " + endId + " is less than start testId: " + startId);
26+
}
27+
validate(startId);
28+
validate(endId);
29+
}
30+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.johnlpage.memex.cucumber.steps;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.JsonNode;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.databind.node.ObjectNode;
7+
import com.johnlpage.memex.model.VehicleInspection;
8+
import io.cucumber.datatable.DataTable;
9+
import io.cucumber.java.en.Then;
10+
import io.cucumber.java.en.When;
11+
import org.assertj.core.api.Assertions;
12+
import org.bson.Document;
13+
import org.springframework.beans.BeanWrapperImpl;
14+
import org.springframework.beans.factory.annotation.Autowired;
15+
import org.springframework.data.mongodb.core.MongoTemplate;
16+
import org.springframework.data.mongodb.core.query.Criteria;
17+
import org.springframework.data.mongodb.core.query.Query;
18+
import org.springframework.kafka.core.KafkaTemplate;
19+
20+
import java.util.HashMap;
21+
import java.util.Iterator;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
26+
public class VehicleInspectionKafkaConsumerSteps {
27+
28+
@Autowired
29+
private KafkaTemplate<String, String> kafkaTemplate;
30+
31+
@Autowired
32+
private MongoTemplate mongoTemplate;
33+
34+
@Autowired
35+
private ObjectMapper objectMapper;
36+
37+
@Autowired
38+
private VehicleInspectionIdRangeValidator idRangeValidator;
39+
40+
@When("I send {int} vehicle inspections starting with id {long} to kafka with:")
41+
public void sendVehicleInspectionsToKafka(int count, long startId, String jsonTemplate) throws JsonProcessingException {
42+
idRangeValidator.validate(startId);
43+
long endIdInclusive = startId + count - 1;
44+
idRangeValidator.validate(endIdInclusive);
45+
46+
for (int i = 0; i < count; i++) {
47+
long testId = startId + i;
48+
VehicleInspection vehicleInspection = objectMapper.readValue(jsonTemplate, VehicleInspection.class);
49+
vehicleInspection.setTestid(testId);
50+
51+
String message = objectMapper.writeValueAsString(vehicleInspection);
52+
kafkaTemplate.send("test", message);
53+
}
54+
}
55+
56+
@Then("verify {int} vehicle inspections are saved starting from id {long} in mongo with:")
57+
public void verifyVehicleInspectionsSaved(int count, long startId, String expectedJson) throws JsonProcessingException {
58+
long endId = startId + count - 1;
59+
idRangeValidator.validateRange(startId, endId);
60+
61+
JsonNode expectedNode = objectMapper.readTree(expectedJson);
62+
63+
Query query = Query.query(Criteria.where("testid").gte(startId).lte(endId));
64+
List<VehicleInspection> inspections = mongoTemplate.find(query, VehicleInspection.class);
65+
66+
Assertions.assertThat(inspections).hasSize(count);
67+
68+
for (VehicleInspection inspection : inspections) {
69+
JsonNode inspectionJson = objectMapper.readTree(objectMapper.writeValueAsString(inspection));
70+
assertJsonContains(expectedNode, inspectionJson);
71+
}
72+
}
73+
74+
75+
private void assertJsonContains(JsonNode expected, JsonNode actual) {
76+
for (Iterator<Map.Entry<String, JsonNode>> it = expected.fields(); it.hasNext(); ) {
77+
Map.Entry<String, JsonNode> field = it.next();
78+
String fieldName = field.getKey();
79+
JsonNode expectedValue = field.getValue();
80+
JsonNode actualValue = actual.get(fieldName);
81+
82+
Assertions.assertThat(actualValue)
83+
.withFailMessage("Expected field '%s' to exist", fieldName)
84+
.isNotNull();
85+
86+
if (expectedValue.isObject()) {
87+
assertJsonContains(expectedValue, actualValue);
88+
} else {
89+
Assertions.assertThat(actualValue).isEqualTo(expectedValue);
90+
}
91+
}
92+
}
93+
94+
}

0 commit comments

Comments
 (0)