Adds an example for an E2E system test that leverages Kafka for JUnit
This commit is contained in:
@@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
@@ -37,9 +38,11 @@ public class ItemsCommandResource {
|
||||
|
||||
log.info("Received a create item request with data {}.", createItem);
|
||||
|
||||
ItemCommand createItemCommand = commandsFor(createItem);
|
||||
|
||||
return commandHandler
|
||||
.onCommand(commandsFor(createItem))
|
||||
.thenApply(dontCare -> ResponseEntity.accepted().build())
|
||||
.onCommand(createItemCommand)
|
||||
.thenApply(dontCare -> ResponseEntity.created(itemUri(createItemCommand.getItemId())).build())
|
||||
.exceptionally(e -> {
|
||||
log.warn("Caught an exception at the service boundary.", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
@@ -49,4 +52,8 @@ public class ItemsCommandResource {
|
||||
private ItemCommand commandsFor(final CreateItemRequest createItem) {
|
||||
return new CreateItem(createItem.getDescription());
|
||||
}
|
||||
|
||||
private URI itemUri(final String itemId) {
|
||||
return URI.create("/items/" + itemId);
|
||||
}
|
||||
}
|
||||
|
||||
88
gtd-e2e-tests/pom.xml
Normal file
88
gtd-e2e-tests/pom.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>net.mguenther.gtd</groupId>
|
||||
<artifactId>gtd-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>gtd-e2e-tests</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<name>gtd-e2e-tests</name>
|
||||
|
||||
<dependencies>
|
||||
<!-- Intra-project dependencies -->
|
||||
<dependency>
|
||||
<groupId>net.mguenther.gtd</groupId>
|
||||
<artifactId>gtd-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.mguenther.gtd</groupId>
|
||||
<artifactId>gtd-codec</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<!-- HTTP client -->
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-httpclient</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-slf4j</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-jaxrs</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>${httpclient.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- Testing -->
|
||||
<dependency>
|
||||
<groupId>net.mguenther.kafka</groupId>
|
||||
<artifactId>kafka-junit</artifactId>
|
||||
<version>${kafka.junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-core</artifactId>
|
||||
<version>${hamcrest.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.mguenther.gtd.client;
|
||||
|
||||
/**
|
||||
* @author Markus Günther (markus.guenther@gmail.com)
|
||||
*/
|
||||
public class CreateItem {
|
||||
|
||||
private final String description;
|
||||
|
||||
public CreateItem(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CreateItemRequest{" +
|
||||
"description='" + description + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.mguenther.gtd.client;
|
||||
|
||||
import feign.Headers;
|
||||
import feign.Param;
|
||||
import feign.RequestLine;
|
||||
import feign.Response;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface GettingThingsDone {
|
||||
|
||||
@RequestLine("POST /items")
|
||||
@Headers("Content-Type: application/json")
|
||||
Response createItem(CreateItem payload);
|
||||
|
||||
@RequestLine("PUT /items/{itemId}")
|
||||
@Headers("Content-Type: application/json")
|
||||
Response updateItem(@Param("itemId") String itemId, UpdateItem payload);
|
||||
|
||||
@RequestLine("GET /items/{itemId}")
|
||||
@Headers("Accept: application/json")
|
||||
Item getItem(@Param("itemId") String itemId);
|
||||
|
||||
@RequestLine("GET /items")
|
||||
@Headers("Accept: application/json")
|
||||
List<Item> getItems();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package net.mguenther.gtd.client;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Markus Günther (markus.guenther@gmail.com)
|
||||
*/
|
||||
public class Item {
|
||||
|
||||
private String id;
|
||||
|
||||
private String description;
|
||||
|
||||
private int requiredTime;
|
||||
|
||||
private Date dueDate;
|
||||
|
||||
private List<String> tags;
|
||||
|
||||
private String associatedList;
|
||||
|
||||
private boolean done;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public int getRequiredTime() {
|
||||
return requiredTime;
|
||||
}
|
||||
|
||||
public Date getDueDate() {
|
||||
return dueDate;
|
||||
}
|
||||
|
||||
public List<String> getTags() {
|
||||
return Collections.unmodifiableList(tags);
|
||||
}
|
||||
|
||||
public String getAssociatedList() {
|
||||
return associatedList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package net.mguenther.gtd.client;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Markus Günther (markus.guenther@gmail.com)
|
||||
*/
|
||||
public class UpdateItem {
|
||||
|
||||
private Long dueDate;
|
||||
|
||||
private Integer requiredTime;
|
||||
|
||||
private List<String> tags;
|
||||
|
||||
private String associatedList;
|
||||
|
||||
public Long getDueDate() {
|
||||
return dueDate;
|
||||
}
|
||||
|
||||
public void setDueDate(long dueDate) {
|
||||
this.dueDate = dueDate;
|
||||
}
|
||||
|
||||
public Integer getRequiredTime() {
|
||||
return requiredTime;
|
||||
}
|
||||
|
||||
public void setRequiredTime(int requiredTime) {
|
||||
this.requiredTime = requiredTime;
|
||||
}
|
||||
|
||||
public List<String> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
public void setTags(List<String> tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
public String getAssociatedList() {
|
||||
return associatedList;
|
||||
}
|
||||
|
||||
public void setAssociatedList(String associatedList) {
|
||||
this.associatedList = associatedList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UpdateItemRequest{" +
|
||||
"dueDate=" + dueDate +
|
||||
", requiredTime=" + requiredTime +
|
||||
", tags=" + tags +
|
||||
", associatedList='" + associatedList + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package net.mguenther.gtd;
|
||||
|
||||
import feign.Feign;
|
||||
import feign.Logger;
|
||||
import feign.Response;
|
||||
import feign.httpclient.ApacheHttpClient;
|
||||
import feign.jackson.JacksonDecoder;
|
||||
import feign.jackson.JacksonEncoder;
|
||||
import feign.slf4j.Slf4jLogger;
|
||||
import net.mguenther.gtd.client.CreateItem;
|
||||
import net.mguenther.gtd.client.GettingThingsDone;
|
||||
import net.mguenther.gtd.domain.event.ItemCreated;
|
||||
import net.mguenther.gtd.kafka.serialization.AvroItemEvent;
|
||||
import net.mguenther.gtd.kafka.serialization.ItemEventConverter;
|
||||
import net.mguenther.gtd.kafka.serialization.ItemEventDeserializer;
|
||||
import net.mguenther.kafka.junit.ExternalKafkaCluster;
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static net.mguenther.kafka.junit.ObserveKeyValues.on;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
public class EventPublicationTest {
|
||||
|
||||
private static final String URL = "http://localhost:8765/api";
|
||||
|
||||
private final ItemEventConverter converter = new ItemEventConverter();
|
||||
|
||||
@Test
|
||||
public void anItemCreatedEventShouldBePublishedAfterCreatingNewItem() throws Exception {
|
||||
|
||||
ExternalKafkaCluster kafka = ExternalKafkaCluster.at("http://localhost:9092");
|
||||
GettingThingsDone gtd = createGetthingThingsDoneClient();
|
||||
String itemId = extractItemId(gtd.createItem(new CreateItem("I gotta do my homework!")));
|
||||
|
||||
List<AvroItemEvent> publishedEvents = kafka
|
||||
.observeValues(on("topic-getting-things-done", 1, AvroItemEvent.class)
|
||||
.observeFor(10, TimeUnit.SECONDS)
|
||||
.with(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ItemEventDeserializer.class)
|
||||
.filterOnKeys(aggregateId -> aggregateId.equals(itemId)));
|
||||
|
||||
ItemCreated itemCreatedEvent = publishedEvents.stream()
|
||||
.findFirst()
|
||||
.map(converter::to)
|
||||
.map(e -> (ItemCreated) e)
|
||||
.orElseThrow(AssertionError::new);
|
||||
|
||||
assertThat(itemCreatedEvent.getItemId(), equalTo(itemId));
|
||||
assertThat(itemCreatedEvent.getDescription(), equalTo("I gotta do my homework!"));
|
||||
}
|
||||
|
||||
private GettingThingsDone createGetthingThingsDoneClient() {
|
||||
return Feign.builder()
|
||||
.client(new ApacheHttpClient())
|
||||
.encoder(new JacksonEncoder())
|
||||
.decoder(new JacksonDecoder())
|
||||
.logger(new Slf4jLogger(GettingThingsDone.class))
|
||||
.logLevel(Logger.Level.FULL)
|
||||
.target(GettingThingsDone.class, URL);
|
||||
}
|
||||
|
||||
private String extractItemId(final Response response) {
|
||||
return response.headers()
|
||||
.get("Location")
|
||||
.stream()
|
||||
.findFirst()
|
||||
.map(s -> s.replace("/items/", ""))
|
||||
.orElseThrow(AssertionError::new);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public class ItemQueryResource {
|
||||
this.itemView = itemView;
|
||||
}
|
||||
|
||||
/*@RequestMapping(path = "/items/{itemId}", method = RequestMethod.GET, produces = "application/json")
|
||||
@RequestMapping(path = "/items/{itemId}", method = RequestMethod.GET, produces = "application/json")
|
||||
public CompletableFuture<ResponseEntity<Item>> showItem(@PathVariable("itemId") String itemId) {
|
||||
|
||||
log.info("Received a show item request for item with ID {}.", itemId);
|
||||
@@ -38,5 +38,5 @@ public class ItemQueryResource {
|
||||
return itemView.getItem(itemId)
|
||||
.thenApply(optionalItem -> optionalItem.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build()))
|
||||
.exceptionally(e -> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build());
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,13 +42,5 @@ public class ItemsQueryResource {
|
||||
.exceptionally(e -> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build());
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/items/{itemId}", method = RequestMethod.GET, produces = "application/json")
|
||||
public CompletableFuture<ResponseEntity<Item>> showItem(@PathVariable("itemId") String itemId) {
|
||||
|
||||
log.info("Received a show item request for item with ID {}.", itemId);
|
||||
|
||||
return itemView.getItem(itemId)
|
||||
.thenApply(optionalItem -> optionalItem.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build()))
|
||||
.exceptionally(e -> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build());
|
||||
}
|
||||
}
|
||||
|
||||
59
pom.xml
59
pom.xml
@@ -21,6 +21,7 @@
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<!-- Managed dependency versions -->
|
||||
<kafka.junit.version>2.7.0</kafka.junit.version>
|
||||
<junit.jupiter.version>5.7.0</junit.jupiter.version>
|
||||
<junit.vintage.version>5.7.0</junit.vintage.version>
|
||||
<junit.version>4.13.1</junit.version>
|
||||
@@ -31,6 +32,8 @@
|
||||
<spring-cloud.version>2020.0.0</spring-cloud.version>
|
||||
<avro.version>1.8.1</avro.version>
|
||||
<commons-lang3.version>3.5</commons-lang3.version>
|
||||
<openfeign.version>11.0</openfeign.version>
|
||||
<httpclient.version>4.5.13</httpclient.version>
|
||||
<!-- Plugin versions -->
|
||||
<plugin.avro.version>1.10.1</plugin.avro.version>
|
||||
<plugin.compiler.version>3.5</plugin.compiler.version>
|
||||
@@ -44,6 +47,7 @@
|
||||
<module>gtd-query-side</module>
|
||||
<module>gtd-api-gateway</module>
|
||||
<module>gtd-discovery-service</module>
|
||||
<module>gtd-e2e-tests</module>
|
||||
</modules>
|
||||
|
||||
<build>
|
||||
@@ -152,11 +156,60 @@
|
||||
<artifactId>log4j-over-slf4j</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
<!-- HTTP client -->
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-core</artifactId>
|
||||
<version>${openfeign.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-httpclient</artifactId>
|
||||
<version>${openfeign.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-slf4j</artifactId>
|
||||
<version>${openfeign.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-jackson</artifactId>
|
||||
<version>${openfeign.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-jaxrs</artifactId>
|
||||
<version>${openfeign.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>${httpclient.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- Testing -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<groupId>net.mguenther.kafka</groupId>
|
||||
<artifactId>kafka-junit</artifactId>
|
||||
<version>${kafka.junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
||||
Reference in New Issue
Block a user