Skip to content

Commit ff7e5d2

Browse files
Merge pull request #1 from rajatsharma2393/cucumber_kafka_tests
Cucumber kafka integration tests
2 parents 1a95151 + b21683c commit ff7e5d2

9 files changed

Lines changed: 199 additions & 40 deletions

File tree

memex/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@
131131
<artifactId>junit-jupiter</artifactId>
132132
<scope>test</scope>
133133
</dependency>
134+
<dependency>
135+
<groupId>org.springframework.kafka</groupId>
136+
<artifactId>spring-kafka-test</artifactId>
137+
<scope>test</scope>
138+
</dependency>
134139
<dependency>
135140
<groupId>org.testcontainers</groupId>
136141
<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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
@TestPropertySource(properties = {
21+
"memex.testcontainers.mongodb.enabled=true",
22+
"memex.kafkaexmple.enabled=true"
23+
})
24+
public class CucumberSpringContextConfig {
25+
26+
@Autowired
27+
private MongoTemplate mongoTemplate;
28+
29+
@Value("${data.test.id.range.start:100}")
30+
private long idStart;
31+
32+
@Value("${data.test.id.range.end:200}")
33+
private long idEnd;
34+
35+
@Before
36+
public void wipeTestData() {
37+
for (String collectionName : mongoTemplate.getCollectionNames()) {
38+
Query query = new Query(Criteria.where("_id").gte(idStart).lt(idEnd));
39+
mongoTemplate.remove(query, collectionName);
40+
}
41+
}
42+
}

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

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,41 @@
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;
1920
import java.time.ZonedDateTime;
2021
import java.time.format.DateTimeFormatter;
22+
import java.util.ArrayList;
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.springframework.beans.factory.annotation.Autowired;
2628
import org.springframework.boot.test.web.server.LocalServerPort;
27-
import org.springframework.test.context.ActiveProfiles;
29+
import org.springframework.data.mongodb.core.MongoTemplate;
30+
import org.springframework.data.mongodb.core.query.Criteria;
31+
import org.springframework.data.mongodb.core.query.Query;
2832

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

3435
@LocalServerPort
3536
private int port;
3637

38+
@Autowired
39+
private MongoTemplate mongoTemplate;
40+
41+
@Autowired
42+
private ObjectMapper objectMapper;
43+
3744
private Response response;
3845
private ZonedDateTime capturedTimestamp;
3946

@@ -47,18 +54,47 @@ public Boolean bool(String bool){
4754
}
4855

4956
@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
57+
public void givenVehicleInspectionsExist(DataTable dataTable) throws JsonProcessingException {
58+
List<Map<String, String>> rows = dataTable.asMaps(String.class, String.class);
59+
List<VehicleInspection> inspections = new ArrayList<>();
60+
61+
for (Map<String, String> row : rows) {
62+
String json = row.get("json");
63+
VehicleInspection inspection = objectMapper.readValue(json, VehicleInspection.class);
64+
inspections.add(inspection);
65+
}
66+
67+
mongoTemplate.insertAll(inspections);
5268
}
5369

5470
@Given("the following vehicle inspections exist and have historical data as of {string}:")
5571
public void givenVehicleInspectionsExist(String date, DataTable dataTable) {
72+
// TODO
5673
// test assumes it exists however it would be better to create it here
5774
}
5875

5976
@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
77+
public void givenTheFollowingVehicleInspectionsDoNotExist(DataTable dataTable) {
78+
for (Map<String, String> row : dataTable.asMaps()) {
79+
80+
if (row.size() != 1) {
81+
throw new IllegalArgumentException("Only one column per row is supported in this step.");
82+
}
83+
84+
String key = row.keySet().iterator().next();
85+
String value = row.get(key);
86+
87+
Query query = switch (key) {
88+
case "testid" -> {
89+
Long testid = Long.parseLong(value);
90+
yield Query.query(Criteria.where("testid").is(testid));
91+
}
92+
case "model" -> Query.query(Criteria.where("vehicle.model").is(value));
93+
default -> throw new UnsupportedOperationException("Unsupported deletion key: " + key);
94+
};
95+
96+
mongoTemplate.remove(query, VehicleInspection.class);
97+
}
6298
}
6399

64100
@Given("I capture the current timestamp")
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.johnlpage.memex.cucumber.steps;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.johnlpage.memex.model.VehicleInspection;
6+
import io.cucumber.java.en.Given;
7+
import io.cucumber.java.en.Then;
8+
import org.assertj.core.api.Assertions;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.data.mongodb.core.MongoTemplate;
11+
import org.springframework.kafka.core.KafkaTemplate;
12+
13+
import java.util.Date;
14+
import java.util.List;
15+
16+
public class VehicleInspectionKafkaConsumerSteps {
17+
18+
@Autowired
19+
private KafkaTemplate<String, String> kafkaTemplate;
20+
21+
@Autowired
22+
private MongoTemplate mongoTemplate;
23+
24+
@Autowired
25+
private ObjectMapper objectMapper;
26+
27+
@Given("{int} vehicle inspections records are sent to kafka with capacity {long}")
28+
public void givenSendKafkaVehicleInspectionRecords(int iterationCount, long capacity) throws JsonProcessingException {
29+
30+
for(int i=0;i<iterationCount;i++) {
31+
VehicleInspection inspection = new VehicleInspection();
32+
inspection.setTestid(91000L+i);
33+
inspection.setTestdate(new Date());
34+
inspection.setTestclass("A"+i);
35+
inspection.setTestresult("PASS");
36+
inspection.setCapacity(capacity);
37+
38+
String message = objectMapper.writeValueAsString(inspection);
39+
kafkaTemplate.send("test", message);
40+
}
41+
}
42+
43+
@Then("verify {int} records are saved in mongo with capacity {long}")
44+
public void theResponseShouldBeAStreamOfValidJsonObjectsEachOnANewLine(int recordsCount, long capacity) {
45+
List<VehicleInspection> savedDocs = mongoTemplate.findAll(VehicleInspection.class);
46+
Assertions.assertThat(savedDocs).hasSize(recordsCount);
47+
Assertions.assertThat(savedDocs.stream().allMatch((vi)->vi.getCapacity().equals(capacity))).isTrue();
48+
}
49+
}

memex/src/test/resources/application-test.properties

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
memex.testcontainers.mongodb.enabled=true
22

3-
spring.data.mongodb.uri=mongodb://localhost:63650
3+
#Keep this empty if you want tests to use Mongo test container
4+
spring.data.mongodb.uri=
45
spring.data.mongodb.database=memex
6+
7+
data.test.id.range.start=1000
8+
data.test.id.range.end=100000
9+
510
server.shutdown=graceful
611
spring.mvc.async.request-timeout=0
712
spring.lifecycle.timeout-per-shutdown-phase=30s

memex/src/test/resources/features/inspections.feature

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ Feature: Vehicle Inspection API Management
6363
@get @by_id @sunny_day
6464
Scenario: Successfully retrieve a vehicle inspection by ID
6565
Given the following vehicle inspections exist:
66-
| testid | vehicle.model |
67-
| 1001 | Corolla |
66+
| json |
67+
| {"testid": 1001, "vehicle": {"model": "Corolla"}} |
6868
When I send a GET request to "/api/inspections/id/1001"
6969
Then the response status code should be 200
7070
And the response should contain "testid": 1001
@@ -136,7 +136,10 @@ Feature: Vehicle Inspection API Management
136136
| REPLACE | true | 2007 | 2008 |
137137

138138
@post @load_stream @sunny_day
139-
Scenario: Successfully load a stream of vehicle inspections with different update strategies and futz options
139+
Scenario: Successfully delete a vehicle inspection
140+
Given the following vehicle inspections exist:
141+
| json |
142+
| {"testid": 2007} |
140143
When I send a POST request to "/api/inspections?updateStrategy=UPDATEWITHHISTORY&futz=true" with the payload:
141144
"""
142145
[
@@ -173,11 +176,11 @@ Feature: Vehicle Inspection API Management
173176
@get @by_model @sunny_day
174177
Scenario Outline: Successfully retrieve vehicle inspections by model with pagination
175178
Given the following vehicle inspections exist:
176-
| testid | vehicle.model |
177-
| 2002 | Focus |
178-
| 2004 | Focus |
179-
| 2006 | Focus |
180-
| 2008 | Focus |
179+
| json |
180+
| {"testid": 2002, "vehicle": {"model": "Focus"}} |
181+
| {"testid": 2004, "vehicle": {"model": "Focus"}} |
182+
| {"testid": 2006, "vehicle": {"model": "Focus"}} |
183+
| {"testid": 2008, "vehicle": {"model": "Focus"}} |
181184
When I send a GET request to "/api/inspections/model/<model>?page=<page>&size=<size>"
182185
Then the response status code should be 200
183186
And the response should contain "content" with <expected_count> items
@@ -204,9 +207,9 @@ Feature: Vehicle Inspection API Management
204207
@post @mongo_query @sunny_day
205208
Scenario: Successfully execute a native MongoDB query with sorting and filter on non-indexed field
206209
Given the following vehicle inspections exist:
207-
| testid | vehicle.make |
208-
| 1001 | Toyota |
209-
| 2002 | Ford |
210+
| json |
211+
| {"testid": 1001, "vehicle": {"make": "Toyota"}} |
212+
| {"testid": 2002, "vehicle": {"make": "Ford"}} |
210213
When I send a POST request to "/api/inspections/query" with the payload:
211214
"""
212215
{
@@ -225,9 +228,9 @@ Feature: Vehicle Inspection API Management
225228
@post @mongo_query @sunny_day
226229
Scenario: Successfully execute a native MongoDB query with sorting and filter on indexed field
227230
Given the following vehicle inspections exist:
228-
| testid | vehicle.model |
229-
| 1001 | Corolla |
230-
| 2002 | Focus |
231+
| json |
232+
| {"testid": 1001, "vehicle": {"model": "Corolla"}} |
233+
| {"testid": 2002, "vehicle": {"model": "Focus"}} |
231234
When I send a POST request to "/api/inspections/query" with the payload:
232235
"""
233236
{
@@ -258,9 +261,9 @@ Feature: Vehicle Inspection API Management
258261
@get @stream_json @sunny_day
259262
Scenario: Successfully stream all vehicle inspections as JSON
260263
Given the following vehicle inspections exist:
261-
| testid | vehicle.make |
262-
| 1001 | Toyota |
263-
| 2002 | Ford |
264+
| json |
265+
| {"testid": 1001, "vehicle": {"make": "Toyota"}} |
266+
| {"testid": 2002, "vehicle": {"make": "Ford"}} |
264267
When I send a GET request to "/api/inspections/json"
265268
Then the response status code should be 200
266269
And the "Content-Type" header should be "application/json"
@@ -270,9 +273,9 @@ Feature: Vehicle Inspection API Management
270273
@get @stream_json_native @sunny_day
271274
Scenario: Successfully stream all vehicle inspections as native JSON
272275
Given the following vehicle inspections exist:
273-
| testid | vehicle.make |
274-
| 1001 | Toyota |
275-
| 2002 | Ford |
276+
| json |
277+
| {"testid": 1001, "vehicle": {"make": "Toyota"}} |
278+
| {"testid": 2002, "vehicle": {"make": "Ford"}} |
276279
When I send a GET request to "/api/inspections/jsonnative"
277280
Then the response status code should be 200
278281
And the "Content-Type" header should be "application/json"
@@ -281,9 +284,9 @@ Feature: Vehicle Inspection API Management
281284

282285
@get @as_of @sunny_day
283286
Scenario: Successfully retrieve vehicle inspection history as of a specific date
284-
Given the following vehicle inspections exist and have historical data as of "2023-10-25 12:00:00":
285-
| testid | vehicle.make |
286-
| 2006 | Ford |
287+
Given the following vehicle inspections exist:
288+
| json |
289+
| {"testid": 2006, "vehicle": {"make": "Ford", "model": "Focus"}} |
287290
And I wait for 1 second
288291
And I capture the current timestamp
289292
And I wait for 1 second
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Feature: Vehicle Inspection Kafka consumer
2+
3+
Scenario: Vehicle Inspection Kafka consumer listens to sent messages
4+
Given 100 vehicle inspections records are sent to kafka with capacity 60
5+
And I wait for 2 second
6+
Then verify 100 records are saved in mongo with capacity 60

0 commit comments

Comments
 (0)