Compare commits

..

2 Commits

Author SHA1 Message Date
Tom Hombergs
d8fa325ae0 updated README 2018-03-17 21:24:18 +01:00
Tom Hombergs
fceb1d898d added pact publish task 2018-03-17 21:21:22 +01:00
755 changed files with 1343 additions and 44159 deletions

3
.gitignore vendored
View File

@@ -1,3 +0,0 @@
**/.idea/
**/*.iml

View File

@@ -1,18 +1,7 @@
before_install:
- chmod +x build-all.sh
- |
if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(.md)|^(LICENSE)'
then
echo "Not running CI since only docs were changed."
exit
fi
install: skip
script:
- ./build-all.sh
- chmod +x gradlew
language: java
jdk:
- oraclejdk11
- oraclejdk8

View File

@@ -2,20 +2,7 @@
[![Travis CI Status](https://travis-ci.org/thombergs/code-examples.svg?branch=master)](https://travis-ci.org/thombergs/code-examples)
This repo contains example projects which show how to use different (not only) Java technologies.
This repo contains example projects which show how to use different java technologies.
The examples are usually accompanied by a blog post on [https://reflectoring.io](https://reflectoring.io).
See the READMEs in each subdirectory of this repo for more information on each module.
## Java Modules
All Java modules require **Java 11** to compile and run.
### Building with Gradle
Each module should be an independent build and can be built by calling `./gradlew clean build` in the module directory.
All modules are listed in [build-all.sh](build-all.sh) to run in the CI pipeline.
### Non-Java Modules
Some folders contain non-Java projects. For those, refer to the README within the module folder.
See the READMEs in each subdirectory of this repo for more information.

View File

@@ -1,32 +0,0 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

View File

@@ -1,5 +0,0 @@
FROM openjdk:8-jdk-alpine
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
EXPOSE 8080

View File

@@ -1,24 +0,0 @@
plugins {
id 'org.springframework.boot' version '2.2.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'io.reflectoring'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}

View File

@@ -1,172 +0,0 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

View File

@@ -1 +0,0 @@
rootProject.name = 'aws-hello-world'

View File

@@ -1,13 +0,0 @@
package io.reflectoring.awshelloworld;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AwsHelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(AwsHelloWorldApplication.class, args);
}
}

View File

@@ -1,14 +0,0 @@
package io.reflectoring.awshelloworld;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloWorldController {
@GetMapping("/hello")
public String helloWorld(){
return "Hello AWS!";
}
}

View File

@@ -1,13 +0,0 @@
package io.reflectoring.awshelloworld;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class AwsHelloWorldApplicationTests {
@Test
void contextLoads() {
}
}

View File

@@ -1,100 +0,0 @@
#!/bin/bash
MAIN_DIR=$PWD
build_gradle_module() {
MODULE_PATH=$1
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH"
echo "+++"
cd $MODULE_PATH && {
chmod +x gradlew
./gradlew clean build --info --stacktrace
if [ $? -ne 0 ]
then
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH FAILED"
echo "+++"
exit 1
else
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH SUCCESSFUL"
echo "+++"
fi
cd $MAIN_DIR
}
}
build_maven_module() {
MODULE_PATH=$1
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH"
echo "+++"
cd $MODULE_PATH && {
chmod +x mvnw
./mvnw clean package
if [ $? -ne 0 ]
then
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH FAILED"
echo "+++"
exit 1
else
echo ""
echo "+++"
echo "+++ BUILDING MODULE $MODULE_PATH SUCCESSFUL"
echo "+++"
fi
cd $MAIN_DIR
}
}
chmod +x gradlew
build_maven_module "spring-boot/spring-boot-openapi"
build_gradle_module "spring-boot/boundaries"
build_gradle_module "spring-boot/argumentresolver"
build_gradle_module "spring-data/spring-data-jdbc-converter"
build_gradle_module "solid"
build_gradle_module "spring-boot/data-migration/flyway"
build_gradle_module "reactive"
build_gradle_module "junit/assumptions"
build_gradle_module "logging"
build_gradle_module "pact/pact-feign-consumer"
# currently disabled since the consumer build won't run
# build_gradle_module "pact/pact-message-consumer"
# build_gradle_module "pact/pact-message-producer"
build_gradle_module "pact/pact-spring-provider"
build_gradle_module "patterns"
build_gradle_module "spring-boot/conditionals"
build_gradle_module "spring-boot/configuration"
build_gradle_module "spring-boot/mocking"
build_gradle_module "spring-boot/modular"
build_gradle_module "spring-boot/paging"
build_gradle_module "spring-boot/rabbitmq-event-brokering"
build_gradle_module "spring-boot/spring-boot-logging"
build_gradle_module "spring-boot/spring-boot-testing"
build_gradle_module "spring-boot/starter"
build_gradle_module "spring-boot/startup"
build_gradle_module "spring-boot/static"
build_gradle_module "spring-boot/validation"
build_gradle_module "spring-boot/profiles"
build_gradle_module "spring-boot/password-encoding"
build_gradle_module "spring-cloud/feign-with-spring-data-rest"
build_gradle_module "spring-cloud/sleuth-downstream-service"
build_gradle_module "spring-cloud/sleuth-upstream-service"
build_gradle_module "spring-cloud/spring-cloud-contract-consumer"
build_gradle_module "spring-cloud/spring-cloud-contract-provider"
build_gradle_module "spring-data/spring-data-rest-associations"
build_gradle_module "spring-data/spring-data-rest-springfox"
build_gradle_module "tools/jacoco"
echo ""
echo "+++"
echo "+++ ALL MODULES SUCCESSFUL"
echo "+++"

View File

@@ -0,0 +1,15 @@
# Consumer-Driven-Contract Test for a Feign Consumer
This repo contains an example of consumer-driven-contract testing for a Feign client
that consumes a REST API provided by the module `pact-spring-data-rest-provider`.
The contract is created and verified with [Pact](https://docs.pact.io/).
## Companion Blog Post
The Companion Blog Post to this project can be found [here](https://reflectoring.io/consumer-driven-contracts-with-pact-feign-spring-data-rest/).
## Running the application
The interesting part in this code base is the class `ConsumerPactVerificationTest`.
You can run the tests with `gradlew test` on Windows or `./gradlew test` on Unix.

View File

@@ -0,0 +1,46 @@
buildscript {
ext {
springBootVersion = '1.5.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenLocal()
mavenCentral()
}
ext {
springCloudVersion = 'Dalston.SR2'
}
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-feign')
// locking transitive guava version to work around "java.lang.IllegalAccessError: tried to access method com.google.common.collect.Lists.cartesianProduct"
compile('com.google.guava:guava:22.0')
compile('org.springframework.boot:spring-boot-starter-hateoas')
testCompile group: 'au.com.dius', name: 'pact-jvm-consumer-junit_2.11', version: '3.5.2'
testCompile('org.springframework.boot:spring-boot-starter-test')
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
bootRun{
jvmArgs = ["-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5006"]
}

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-bin.zip

View File

View File

@@ -0,0 +1,24 @@
package com.example.demo;
public class Address {
private long id;
private String street;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}

View File

@@ -0,0 +1,19 @@
package com.example.demo;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(value = "addresses", path = "/addresses")
public interface AddressClient {
@RequestMapping(method = RequestMethod.GET, path = "/")
Resources<Address> getAddresses();
@RequestMapping(method = RequestMethod.GET, path = "/{id}")
Resource<Address> getAddress(@PathVariable("id") long id);
}

View File

@@ -0,0 +1,24 @@
package com.example.demo;
public class Customer {
private long id;
private String name;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,19 @@
package com.example.demo;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(value = "customers", path = "/customers_mto")
public interface CustomerClient {
@RequestMapping(method = RequestMethod.GET, value = "/")
Resources<Customer> getCustomers();
@RequestMapping(method = RequestMethod.GET, value = "/{id}")
Resource<Customer> getCustomer(@PathVariable("id") long id);
}

View File

@@ -0,0 +1,16 @@
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.hateoas.config.EnableHypermediaSupport;
@SpringBootApplication
@EnableFeignClients
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -0,0 +1,15 @@
package com.example.demo;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfiguration {
@Bean
public Logger.Level logLevel(){
return Logger.Level.FULL;
}
}

View File

@@ -0,0 +1,17 @@
server.port: 8081
logging.level.com.example.demo.CustomerClient: DEBUG
logging.level.com.example.demo.AddressClient: DEBUG
logging.level.org.hibernate.SQL: DEBUG
customers:
ribbon:
eureka:
enabled: false
listOfServers: localhost:8080
addresses:
ribbon:
eureka:
enabled: false
listOfServers: localhost:8080

View File

@@ -0,0 +1,137 @@
package com.example.demo;
import au.com.dius.pact.consumer.Pact;
import au.com.dius.pact.consumer.PactProviderRuleMk2;
import au.com.dius.pact.consumer.PactVerification;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.model.RequestResponsePact;
import org.apache.http.entity.ContentType;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
// overriding provider address
"addresses.ribbon.listOfServers: localhost:8888"
})
public class ConsumerPactVerificationTest {
@Rule
public PactProviderRuleMk2 stubProvider = new PactProviderRuleMk2("customerServiceProvider", "localhost", 8888, this);
@Autowired
private AddressClient addressClient;
@Pact(state = "a collection of 2 addresses", provider = "customerServiceProvider", consumer = "addressClient")
public RequestResponsePact createAddressCollectionResourcePact(PactDslWithProvider builder) {
return builder
.given("a collection of 2 addresses")
.uponReceiving("a request to the address collection resource")
.path("/addresses/")
.method("GET")
.willRespondWith()
.status(200)
.body("{\n" +
" \"_embedded\": {\n" +
" \"addresses\": [\n" +
" {\n" +
" \"street\": \"Elm Street\",\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1\"\n" +
" },\n" +
" \"address\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1\"\n" +
" },\n" +
" \"customer\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1/customer\"\n" +
" }\n" +
" }\n" +
" },\n" +
" {\n" +
" \"street\": \"High Street\",\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
" \"href\": \"http://localhost:8080/addresses/2\"\n" +
" },\n" +
" \"address\": {\n" +
" \"href\": \"http://localhost:8080/addresses/2\"\n" +
" },\n" +
" \"customer\": {\n" +
" \"href\": \"http://localhost:8080/addresses/2/customer\"\n" +
" }\n" +
" }\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
" \"href\": \"http://localhost:8080/addresses{?page,size,sort}\",\n" +
" \"templated\": true\n" +
" },\n" +
" \"profile\": {\n" +
" \"href\": \"http://localhost:8080/profile/addresses\"\n" +
" },\n" +
" \"search\": {\n" +
" \"href\": \"http://localhost:8080/addresses/search\"\n" +
" }\n" +
" },\n" +
" \"page\": {\n" +
" \"size\": 20,\n" +
" \"totalElements\": 2,\n" +
" \"totalPages\": 1,\n" +
" \"number\": 0\n" +
" }\n" +
"}", "application/hal+json")
.toPact();
}
@Pact(state = "a single address", provider = "customerServiceProvider", consumer = "addressClient")
public RequestResponsePact createAddressResourcePact(PactDslWithProvider builder) {
return builder
.given("a single address")
.uponReceiving("a request to the address resource")
.path("/addresses/1")
.method("GET")
.willRespondWith()
.status(200)
.body("{\n" +
" \"street\": \"Elm Street\",\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1\"\n" +
" },\n" +
" \"address\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1\"\n" +
" },\n" +
" \"customer\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1/customer\"\n" +
" }\n" +
" }\n" +
"}", "application/hal+json")
.toPact();
}
@Test
@PactVerification(fragment = "createAddressCollectionResourcePact")
public void verifyAddressCollectionPact() {
Resources<Address> addresses = addressClient.getAddresses();
assertThat(addresses).hasSize(2);
}
@Test
@PactVerification(fragment = "createAddressResourcePact")
public void verifyAddressPact() {
Resource<Address> address = addressClient.getAddress(1L);
assertThat(address).isNotNull();
}
}

View File

@@ -1,4 +1,4 @@
package io.reflectoring.validation;
package com.example.demo;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -7,7 +7,7 @@ import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationApplicationTests {
public class DemoApplicationTests {
@Test
public void contextLoads() {

View File

@@ -0,0 +1,16 @@
# Consumer-Driven-Contract Test for a Spring Data Rest Provider
This repo contains an example of consumer-driven-contract testing for a Spring
Data REST API provider. The corresponding consumer to the contract is
implemented in the module `pact-feign-consumer`.
The contract is created and verified with [Pact](https://docs.pact.io/).
## Companion Blog Post
The Companion Blog Post to this project can be found [here](https://reflectoring.io/consumer-driven-contracts-with-pact-feign-spring-data-rest/).
## Running the application
The interesting part in this code base is the class `ProviderPactVerificationTest`.
You can run the tests with `gradlew test` on Windows or `./gradlew test` on Unix.

View File

@@ -0,0 +1,35 @@
buildscript {
ext {
springBootVersion = '1.5.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-data-rest')
compile group: 'au.com.dius', name: 'pact-jvm-provider-junit_2.11', version: '3.5.2'
compile('com.h2database:h2:1.4.196')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
bootRun{
jvmArgs = ["-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"]
}

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-bin.zip

View File

@@ -0,0 +1,43 @@
package com.example.demo;
import javax.persistence.*;
@Entity
public class Address {
@GeneratedValue
@Id
private Long id;
@Column
private String street;
@ManyToOne
private Customer customer;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
}

View File

@@ -0,0 +1,14 @@
package com.example.demo;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.hateoas.Resources;
import java.util.List;
@RepositoryRestResource(path = "addresses")
public interface AddressRepository extends PagingAndSortingRepository<Address, Long> {
List<Address> findByCustomerId(long customerId);
}

View File

@@ -0,0 +1,30 @@
package com.example.demo;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue
private long id;
@Column
private String name;
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,9 @@
package com.example.demo;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource(path = "customers")
public interface CustomerRepository extends CrudRepository<Customer, Long> {
}

View File

@@ -1,13 +1,12 @@
package io.reflectoring;
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -0,0 +1,7 @@
spring.datasource.url=jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
logging.level.org.hibernate.SQL=OFF

View File

@@ -0,0 +1,41 @@
package com.example.demo;
import au.com.dius.pact.provider.junit.PactRunner;
import au.com.dius.pact.provider.junit.Provider;
import au.com.dius.pact.provider.junit.State;
import au.com.dius.pact.provider.junit.loader.PactFolder;
import au.com.dius.pact.provider.junit.target.HttpTarget;
import au.com.dius.pact.provider.junit.target.Target;
import au.com.dius.pact.provider.junit.target.TestTarget;
import com.example.framework.DatabaseStateHolder;
import com.example.framework.SpringBootStarter;
import org.junit.ClassRule;
import org.junit.runner.RunWith;
@RunWith(PactRunner.class)
@Provider("customerServiceProvider")
@PactFolder("../pact-feign-consumer/target/pacts")
public class ProviderPactVerificationTest {
@ClassRule
public static SpringBootStarter appStarter = SpringBootStarter.builder()
.withApplicationClass(DemoApplication.class)
.withArgument("--spring.config.location=classpath:/application-pact.properties")
.withDatabaseState("single-address", "/initial-schema.sql", "/single-address.sql")
.withDatabaseState("address-collection", "/initial-schema.sql", "/address-collection.sql")
.build();
@State("a single address")
public void toSingleAddressState() {
DatabaseStateHolder.setCurrentDatabaseState("single-address");
}
@State("a collection of 2 addresses")
public void toAddressCollectionState() {
DatabaseStateHolder.setCurrentDatabaseState("address-collection");
}
@TestTarget
public final Target target = new HttpTarget(8080);
}

View File

@@ -0,0 +1,32 @@
package com.example.framework;
/**
* Defines a state of the database, which is defined by a set of SQL scripts.
*/
public class DatabaseState {
private final String stateName;
private final String[] sqlscripts;
/**
* Constructor.
*
* @param stateName unique name of this database state.
* @param sqlscripts paths to SQL scripts within the classpath. These scripts will be executed to put the
* database into the database state described by this object.
*/
public DatabaseState(String stateName, String... sqlscripts) {
this.stateName = stateName;
this.sqlscripts = sqlscripts;
}
public String getStateName() {
return stateName;
}
public String[] getSqlscripts() {
return sqlscripts;
}
}

View File

@@ -0,0 +1,36 @@
package com.example.framework;
/**
* Holds the current database state.
* <p/>
* TODO: replace the static state variable with a thread-safe alternative. Potentially use a special HTTP header
* that is intercepted by a Bean defined in {@link PactDatabaseStatesAutoConfiguration} and sets the database
* state in a {@link ThreadLocal} variable.
*/
public class DatabaseStateHolder {
private static String currentDatabaseState;
/**
* Sets the database to the state with the specified name.
* <p/>
* <strong>WARNING:</strong> the database state is not thread safe. If there are multiple threads accessing
* the database in different states at the same time, apocalypse will come!
*
* @param databaseStateName the name of the {@link DatabaseState} to put the database in.
*/
public static void setCurrentDatabaseState(String databaseStateName) {
currentDatabaseState = databaseStateName;
}
/**
* Returns the name of the current {@link DatabaseState}.
* <p/>
* <strong>WARNING:</strong> the database state is not thread safe. If there are multiple threads accessing
* the database in different states at the same time, apocalypse will come!
*/
public static String getCurrentDatabaseState() {
return currentDatabaseState;
}
}

View File

@@ -0,0 +1,50 @@
package com.example.framework;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.util.List;
/**
* Initializes a {@link DataSource} to a specified List of {@link DatabaseState}s. It is assumed that the {@link DataSource}
* can switch between several states.
*/
public class DatabaseStatesInitializer {
private final DataSource dataSource;
private final List<DatabaseState> databaseStates;
/**
* Constructor.
*
* @param dataSource the {@link DataSource} to execute SQL scripts against.
* @param databaseStates the {@link DatabaseState}s to create within the {@link DataSource}.
*/
public DatabaseStatesInitializer(DataSource dataSource, List<DatabaseState> databaseStates) {
this.dataSource = dataSource;
this.databaseStates = databaseStates;
}
/**
* Executes SQL scripts to initialize the {@link DataSource} with several states.
* <p/>
* For each {@link DatabaseState}, the {@link DatabaseStateHolder} will be called to set the {@link DataSource}
* into that state. Then, the SQL scripts of that {@link DatabaseState} are executed against the {@link DataSource}
* to initialize that state.
*/
@PostConstruct
public void initialize() {
for (DatabaseState databaseState : this.databaseStates) {
DatabaseStateHolder.setCurrentDatabaseState(databaseState.getStateName());
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
for (String script : databaseState.getSqlscripts()) {
populator.addScript(new ClassPathResource(script));
}
populator.execute(this.dataSource);
}
}
}

View File

@@ -0,0 +1,70 @@
package com.example.framework;
import au.com.dius.pact.provider.junit.PactRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration AutoConfiguration} that is activated when
* the pact-jvm-provider-junit module is in the classpath.
* </p>
* <p>
* This configuration provides a {@link DataSource} which allows to switch between multiple database states.
* Each database state is defined by a name and a set of SQL scripts which set the database into the desired state.
* The database states are configured via properties:
* <pre>
* pact.databaseStates.&lt;NAME&gt;=/path/to/script1.sql,/path/to/script2.sql,...
* </pre>
* The NAME of the databaseState can be used with {@link DatabaseStateHolder#setCurrentDatabaseState(String)}
* to set the {@link DataSource} into that state.
* </p>
*/
@Configuration
@ConditionalOnClass(PactRunner.class)
@EnableConfigurationProperties(PactProperties.class)
public class PactDatabaseStatesAutoConfiguration {
@Bean
public DataSource dataSource(PactProperties pactProperties) {
AbstractRoutingDataSource dataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseStateHolder.getCurrentDatabaseState();
}
};
Map<Object, Object> targetDataSources = new HashMap<>();
dataSource.setTargetDataSources(targetDataSources);
// create a DataSource for each DatabaseState
for (DatabaseState databaseState : pactProperties.getDatabaseStatesList()) {
DataSource ds = DataSourceBuilder
.create()
.url(String.format("jdbc:h2:mem:%s;DB_CLOSE_ON_EXIT=FALSE", databaseState.getStateName()))
.driverClassName("org.h2.Driver")
.username("sa")
.password("")
.build();
targetDataSources.put(databaseState.getStateName(), ds);
DatabaseStateHolder.setCurrentDatabaseState(databaseState.getStateName());
}
return dataSource;
}
@Bean
public DatabaseStatesInitializer databaseStatesInitializer(DataSource routingDataSource, PactProperties pactProperties) {
return new DatabaseStatesInitializer(routingDataSource, pactProperties.getDatabaseStatesList());
}
}

View File

@@ -0,0 +1,64 @@
package com.example.framework;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Loads the properties "pact.databaseStates.&lt;NAME&gt;" into the Spring environment.
*/
@ConfigurationProperties("pact")
public class PactProperties {
private Map<String, String> databaseStates;
/**
* Retrieves a map with the names of the configured database states as keys and {@link DatabaseState} objects
* as values.
*/
public List<DatabaseState> getDatabaseStatesList() {
List<DatabaseState> databaseStatesList = new ArrayList<>();
for (Map.Entry<String, String> entry : databaseStates.entrySet()) {
String stateName = entry.getKey();
// When reading a property as a Map, as is done for databaseStates, Spring Boot automatically adds numeric
// keys and moves the actual key into the value, separated by a comma. Thus, we have all entries duplicated
// and have to remove the entries with numeric keys.
if (!stateName.matches("^[0-9]+$")) {
String sqlScriptsString = entry.getValue();
String[] sqlScripts = sqlScriptsString.split(",");
databaseStatesList.add(new DatabaseState(stateName, sqlScripts));
}
}
return databaseStatesList;
}
static List<String> toCommandLineArguments(List<DatabaseState> databaseStates) {
List<String> args = new ArrayList<>();
for (DatabaseState databaseState : databaseStates) {
String argString = String.format("--pact.databaseStates.%s=", databaseState.getStateName());
int i = 0;
for (String scriptPath : databaseState.getSqlscripts()) {
argString += String.format("%s", scriptPath);
if (i < databaseState.getSqlscripts().length - 1) {
argString += ",";
}
i++;
}
args.add(argString);
}
return args;
}
public Map<String, String> getDatabaseStates() {
return databaseStates;
}
public void setDatabaseStates(Map<String, String> databaseStates) {
this.databaseStates = databaseStates;
}
}

View File

@@ -0,0 +1,111 @@
package com.example.framework;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* Starts a Spring Boot application.
* </p>
* <p>
* When included in a JUnit test with the {@link org.junit.ClassRule} annotation as in the example below, the Spring Boot application will be
* started before any of the test methods are run.
* <pre>
* public class MyTest {
*
* &#064;ClassRule
* public static SpringBootStarter starter = SpringBootStarter.builder()
* .withApplicationClass(MyApplication.class)
* ...
* .build();
*
* &#064;Test
* public void test(){
* ...
* }
*
* }
* </pre>
* </p>
*/
public class SpringBootStarter implements TestRule {
private final Class<?> applicationClass;
private final List<String> args;
private final List<DatabaseState> databaseStates;
/**
* Constructor.
*
* @param applicationClass the Spring Boot application class.
* @param databaseStates list containing {@link DatabaseState} objects, each describing a database state
* in form of one or more SQL scripts.
* @param args the command line arguments the application should be started with.
*/
public SpringBootStarter(Class<?> applicationClass, List<DatabaseState> databaseStates, List<String> args) {
this.args = args;
this.applicationClass = applicationClass;
this.databaseStates = databaseStates;
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
List<String> args = new ArrayList<>();
args.addAll(SpringBootStarter.this.args);
args.addAll(PactProperties.toCommandLineArguments(SpringBootStarter.this.databaseStates));
ApplicationContext context = SpringApplication.run(SpringBootStarter.this.applicationClass, args.toArray(new String[]{}));
base.evaluate();
SpringApplication.exit(context);
}
};
}
/**
* Creates a builder that provides a fluent API to create a new {@link SpringBootStarter} instance.
*/
public static SpringBootStarterBuilder builder() {
return new SpringBootStarterBuilder();
}
public static class SpringBootStarterBuilder {
private Class<?> applicationClass;
private List<String> args = new ArrayList<>();
private List<DatabaseState> databaseStates = new ArrayList<>();
public SpringBootStarterBuilder withApplicationClass(Class<?> clazz) {
this.applicationClass = clazz;
return this;
}
public SpringBootStarterBuilder withArgument(String argument) {
this.args.add(argument);
return this;
}
public SpringBootStarterBuilder withDatabaseState(String stateName, String... sqlScripts) {
this.databaseStates.add(new DatabaseState(stateName, sqlScripts));
return this;
}
public SpringBootStarter build() {
return new SpringBootStarter(this.applicationClass, this.databaseStates, args);
}
}
}

View File

@@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.framework.PactDatabaseStatesAutoConfiguration

View File

@@ -0,0 +1,2 @@
insert into address (id, street) values (1, 'Elm Street');
insert into address (id, street) values (2, 'High Street');

View File

@@ -0,0 +1,6 @@
server.port=8080
#spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=none
pact.databaseStates[0]=single-address,/initial-schema.sql,/single-address.sql
pact.databaseStates[1]=address-collection,/initial-schema.sql,/address-collection.sql

View File

@@ -0,0 +1,11 @@
create table CUSTOMER (
id NUMBER,
name VARCHAR
);
create table ADDRESS (
id NUMBER,
customer_id NUMBER,
street VARCHAR,
FOREIGN KEY (customer_id) REFERENCES CUSTOMER(id)
);

View File

@@ -0,0 +1 @@
insert into address (id, street) values (1, 'Elm Street');

View File

@@ -1,5 +1,6 @@
#Sun Jul 30 16:58:54 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-all.zip

View File

@@ -7,7 +7,7 @@ buildscript {
apply plugin: 'java'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 11
sourceCompatibility = 1.8
repositories {
mavenLocal()
@@ -23,6 +23,3 @@ dependencies {
testCompile 'junit:junit:4.12'
}
test {
useJUnitPlatform()
}

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-bin.zip

View File

@@ -1,7 +0,0 @@
# Logging Code Examples
## Related Blog Articles
* [Use Logging Levels Consistently](https://reflectoring.io/logging-levels/)
* [Use a Human-Readable Logging Format](https://reflectoring.io/logging-format/)
* [Configuring a Human-Readable Logging Format with Logback and Descriptive Logger](https://reflectoring.io/logging-format-logback/)

View File

@@ -1,20 +0,0 @@
apply plugin: 'java'
buildscript {
repositories {
mavenLocal()
jcenter()
}
}
repositories {
mavenLocal()
jcenter()
}
dependencies {
compile("ch.qos.logback:logback-classic:1.2.3")
compile("io.reflectoring:descriptive-logger:1.0")
testCompile("org.junit.jupiter:junit-jupiter-engine:5.0.1")
}

View File

@@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip

View File

@@ -1,18 +0,0 @@
package io.reflectoring.logging;
import io.reflectoring.descriptivelogger.LoggerFactory;
import org.junit.jupiter.api.Test;
public class LoggingFormatTest {
private MyLogger logger = LoggerFactory.getLogger(MyLogger.class, LoggingFormatTest.class);
@Test
public void testLogPattern(){
Thread.currentThread().setName("very-long-thread-name");
logger.logDebugMessage();
Thread.currentThread().setName("short");
logger.logInfoMessage();
logger.logMessageWithLongId();
}
}

View File

@@ -1,19 +0,0 @@
package io.reflectoring.logging;
import io.reflectoring.descriptivelogger.DescriptiveLogger;
import io.reflectoring.descriptivelogger.LogMessage;
import org.slf4j.event.Level;
@DescriptiveLogger
public interface MyLogger {
@LogMessage(level=Level.DEBUG, message="This is a DEBUG message.", id=14556)
void logDebugMessage();
@LogMessage(level=Level.INFO, message="This is an INFO message.", id=5456)
void logInfoMessage();
@LogMessage(level=Level.ERROR, message="This is an ERROR message with a very long ID.", id=1548654)
void logMessageWithLongId();
}

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<conversionRule conversionWord="truncatedThread"
converterClass="io.reflectoring.logging.TruncatedThreadConverter" />
<conversionRule conversionWord="truncatedLogger"
converterClass="io.reflectoring.logging.TruncatedLoggerConverter" />
<!-- Appender to log to console -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd} | %d{HH:mm:ss.SSS} | %-20.20thread | %5p | %-25.25logger{25} | %12(ID: %8mdc{id}) | %m%n</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>

View File

@@ -4,9 +4,6 @@ This example project shows how to setup an Angular application to use [Pact](htt
in order to create Pact files from a consumer test and validate the
a consumer against the Pact.
## Relevant Blog Post
[Creating a Consumer-Driven Contract with Angular and Pact](https://reflectoring.io/consumer-driven-contracts-with-angular-and-pact/)
## Key Files
* [`user.service.ts`](src/app/user.service.ts): Angular service that calls a REST

View File

@@ -1,11 +1,11 @@
# Testing a Spring Boot REST API Consumer against a Contract with Pact
# Creating a Consumer-Driven Contract with Feign and Pact
## Companion Blog Article
Read the [companion blog article](http://localhost:4000/consumer-driven-contract-feign-pact/) to this repository.
Read the [companion blog article](https://reflectoring.io/consumer-driven-contract-feign-pact/) to this repository.
## Getting Started
* have a look at the [feign client](src/main/java/io/reflectoring/UserClient.java)
* have a look at the [consumer test](src/test/java/io/reflectoring/UserServiceConsumerTest.java)
* run `./gradlew build` in this project to create a pact and run the consumer test
* afterwards, find the pact contract file in the folder `target/pacts`
* have a look at the [feign client](/src/main/java/io/reflectoring/UserClient.java)
* have a look at the [consumer test](/src/test/java/io/reflectoring/UserServiceConsumerTest.java)
* run `./gradlew build` to run all tests and create pact files into the folder `target/pacts`
* run `./gradlew pactPublish` to publish the pact files to a Pact Broker (must specify Pact Broker location and credentials in `build.gradle`)

View File

@@ -0,0 +1,5 @@
userservice:
ribbon:
eureka:
enabled: false
listOfServers: localhost:8080

View File

@@ -9,12 +9,10 @@ buildscript {
}
plugins {
id "au.com.dius.pact" version "3.5.20"
id "au.com.dius.pact" version "3.5.13"
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
version '1.0.0.SNAPSHOT'
@@ -23,41 +21,21 @@ repositories {
jcenter()
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springcloud_version}"
}
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.cloud:spring-cloud-starter-openfeign')
compile('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')
compile("org.springframework.boot:spring-boot-starter-data-jpa:${springboot_version}")
compile("org.springframework.boot:spring-boot-starter-web:${springboot_version}")
compile("org.springframework.cloud:spring-cloud-starter-feign:1.4.1.RELEASE")
compile('com.h2database:h2:1.4.196')
// add jaxb since it's no longer available in Java 11
runtime('javax.xml.bind:jaxb-api:2.3.1')
// add javassist >= 3.23.1-GA since earlier versions are broken in Java 11
// see https://github.com/jboss-javassist/javassist/issues/194
runtime('org.javassist:javassist:3.23.1-GA')
testCompile('org.codehaus.groovy:groovy-all:2.4.6')
testCompile("au.com.dius:pact-jvm-consumer-junit5_2.12:${pact_version}")
testCompile('org.springframework.boot:spring-boot-starter-test')
testRuntimeOnly( 'org.junit.jupiter:junit-jupiter-engine:5.1.0')
testCompile("au.com.dius:pact-jvm-consumer-junit_2.11:3.5.2")
testCompile("org.springframework.boot:spring-boot-starter-test:${springboot_version}")
}
pact {
publish {
pactDirectory = 'target/pacts'
pactBrokerUrl = 'TODO'
pactBrokerUsername = 'TODO'
pactBrokerPassword = 'TODO'
pactBrokerUrl = 'URL'
pactBrokerUsername = 'USERNAME'
pactBrokerPassword = 'PASSWORD'
}
}
test {
useJUnitPlatform()
}

View File

@@ -1,3 +1,2 @@
springboot_version=2.0.4.RELEASE
springcloud_version=Finchley.SR1
pact_version=3.5.20
springboot_version=1.5.9.RELEASE
verifier_version=1.2.2.RELEASE

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-bin.zip

View File

@@ -2,12 +2,10 @@ package io.reflectoring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
@RibbonClient(name = "userservice", configuration = RibbonConfiguration.class)
public class ConsumerApplication {
public static void main(String[] args) {

View File

@@ -1,15 +0,0 @@
package io.reflectoring;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(IClientConfig config) {
return new RandomRule();
}
}

View File

@@ -1,6 +1,6 @@
package io.reflectoring;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

View File

@@ -1,88 +1,83 @@
package io.reflectoring;
import au.com.dius.pact.consumer.Pact;
import au.com.dius.pact.consumer.PactProviderRuleMk2;
import au.com.dius.pact.consumer.PactVerification;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import au.com.dius.pact.model.RequestResponsePact;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.client.RestTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.*;
@ExtendWith(PactConsumerTestExt.class)
@ExtendWith(SpringExtension.class)
@PactTestFor(providerName = "userservice", port = "8888")
@SpringBootTest({
// overriding provider address
"userservice.ribbon.listOfServers: localhost:8888"
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
// overriding provider address
"userservice.ribbon.listOfServers: localhost:8888"
})
class UserServiceConsumerTest {
public class UserServiceConsumerTest {
@Autowired
private UserClient userClient;
@Rule
public PactProviderRuleMk2 stubProvider = new PactProviderRuleMk2("userservice", "localhost", 8888, this);
@Pact(state = "provider accepts a new person", provider = "userservice", consumer = "userclient")
RequestResponsePact createPersonPact(PactDslWithProvider builder) {
@Autowired
private UserClient userClient;
// @formatter:off
return builder
.given("provider accepts a new person")
.uponReceiving("a request to POST a person")
.path("/user-service/users")
.method("POST")
.willRespondWith()
.status(201)
.matchHeader("Content-Type", "application/json")
.body(new PactDslJsonBody()
.integerType("id", 42))
.toPact();
// @formatter:on
}
@Pact(provider = "userservice", consumer = "userclient")
public RequestResponsePact createPersonPact(PactDslWithProvider builder) {
return builder
.given("provider accepts a new person")
.uponReceiving("a request to POST a person")
.path("/user-service/users")
.method("POST")
.willRespondWith()
.status(201)
.matchHeader("Content-Type", "application/json")
.body(new PactDslJsonBody()
.integerType("id", 42))
.toPact();
}
@Pact(state = "person 42 exists", provider = "userservice", consumer = "userclient")
RequestResponsePact updatePersonPact(PactDslWithProvider builder) {
// @formatter:off
return builder
.given("person 42 exists")
.uponReceiving("a request to PUT a person")
.path("/user-service/users/42")
.method("PUT")
.willRespondWith()
.status(200)
.matchHeader("Content-Type", "application/json")
.body(new PactDslJsonBody()
.stringType("firstName", "Zaphod")
.stringType("lastName", "Beeblebrox"))
.toPact();
// @formatter:on
}
@Pact(provider = "userservice", consumer = "userclient")
public RequestResponsePact updatePersonPact(PactDslWithProvider builder) {
return builder
.given("person 42 exists")
.uponReceiving("a request to PUT a person")
.path("/user-service/users/42")
.method("PUT")
.willRespondWith()
.status(200)
.matchHeader("Content-Type", "application/json")
.body(new PactDslJsonBody()
.stringType("firstName", "Zaphod")
.stringType("lastName", "Beeblebrox"))
.toPact();
}
@Test
@PactTestFor(pactMethod = "createPersonPact")
void verifyCreatePersonPact() {
User user = new User();
user.setFirstName("Zaphod");
user.setLastName("Beeblebrox");
IdObject id = userClient.createUser(user);
assertThat(id.getId()).isEqualTo(42);
}
@Test
@PactVerification(fragment = "createPersonPact")
public void verifyCreatePersonPact() {
User user = new User();
user.setFirstName("Zaphod");
user.setLastName("Beeblebrox");
IdObject id = userClient.createUser(user);
assertThat(id.getId()).isEqualTo(42);
}
@Test
@PactTestFor(pactMethod = "updatePersonPact")
void verifyUpdatePersonPact() {
User user = new User();
user.setFirstName("Zaphod");
user.setLastName("Beeblebrox");
User updatedUser = userClient.updateUser(42L, user);
assertThat(updatedUser.getFirstName()).isEqualTo("Zaphod");
assertThat(updatedUser.getLastName()).isEqualTo("Beeblebrox");
}
@Test
@PactVerification(fragment = "updatePersonPact")
public void verifyUpdatePersonPact() {
User user = new User();
user.setFirstName("Zaphod");
user.setLastName("Beeblebrox");
User updatedUser = userClient.updateUser(42L, user);
assertThat(updatedUser.getFirstName()).isEqualTo("Zaphod");
assertThat(updatedUser.getLastName()).isEqualTo("Beeblebrox");
}
}

View File

@@ -1,115 +0,0 @@
{
"provider": {
"name": "userservice"
},
"consumer": {
"name": "userclient"
},
"interactions": [
{
"description": "a request to PUT a person",
"request": {
"method": "PUT",
"path": "/user-service/users/42"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"firstName": "Zaphod",
"lastName": "Beeblebrox"
},
"matchingRules": {
"body": {
"$.firstName": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.lastName": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
},
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json"
}
],
"combine": "AND"
}
}
}
},
"providerStates": [
{
"name": "person 42 exists"
}
]
},
{
"description": "a request to POST a person",
"request": {
"method": "POST",
"path": "/user-service/users"
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json"
},
"body": {
"id": 42
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json"
}
],
"combine": "AND"
}
},
"body": {
"$.id": {
"matchers": [
{
"match": "integer"
}
],
"combine": "AND"
}
}
}
},
"providerStates": [
{
"name": "provider accepts a new person"
}
]
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.20"
}
}
}

View File

@@ -1,7 +0,0 @@
# Consumer-Driven-Contract Test for a Spring Boot Message Consumer
This module shows how to use Pact to implement a contract test for a message provider.
## Companion Articles
[Testing a Spring Message Producer and Consumer against a Contract with Pact](https://reflectoring.io/cdc-pact-messages/)

View File

@@ -1 +0,0 @@
spring.rabbitmq.connection-timeout=10

View File

@@ -1,43 +0,0 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 11
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-amqp')
compile('com.h2database:h2:1.4.196')
compileOnly('org.projectlombok:lombok:1.18.2')
// add jaxb since it's no longer available in Java 11
runtime('javax.xml.bind:jaxb-api:2.3.1')
// add javassist >= 3.23.1-GA since earlier versions are broken in Java 11
// see https://github.com/jboss-javassist/javassist/issues/194
runtime('org.javassist:javassist:3.23.1-GA')
testCompile("au.com.dius:pact-jvm-consumer-junit_2.12:${pact_version}")
testCompile("au.com.dius:pact-jvm-consumer-groovy_2.12:${pact_version}")
testCompile('org.springframework.boot:spring-boot-starter-test')
}
bootRun {
jvmArgs = ["-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"]
}

View File

@@ -1,2 +0,0 @@
springboot_version=2.0.4.RELEASE
pact_version=3.5.20

View File

@@ -1,15 +0,0 @@
package io.reflectoring;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@SpringBootApplication
@EnableRabbit
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -1,37 +0,0 @@
package io.reflectoring;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import java.io.IOException;
import java.util.Set;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MessageConsumer {
private Logger logger = LoggerFactory.getLogger(MessageConsumer.class);
private ObjectMapper objectMapper;
public MessageConsumer(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public void consumeStringMessage(String messageString) throws IOException {
logger.info("Consuming message '{}'", messageString);
UserCreatedMessage message = objectMapper.readValue(messageString, UserCreatedMessage.class);
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<UserCreatedMessage>> violations = validator.validate(message);
if(!violations.isEmpty()){
throw new ConstraintViolationException(violations);
}
// pass message into business use case
}
}

View File

@@ -1,60 +0,0 @@
package io.reflectoring;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MessageConsumerConfiguration {
private static final String QUEUE_NAME = "myQueue";
private static final String EXCHANGE_NAME = "myExchange";
@Bean
public TopicExchange receiverExchange() {
return new TopicExchange(EXCHANGE_NAME);
}
@Bean
public Queue queue() {
return new Queue(QUEUE_NAME);
}
@Bean
public Binding binding(Queue eventReceivingQueue, TopicExchange receiverExchange) {
return BindingBuilder
.bind(eventReceivingQueue)
.to(receiverExchange)
.with("*.*");
}
@Bean
public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConsumerStartTimeout(1000); // we don't want to wait in this example project
container.setConnectionFactory(connectionFactory);
container.setQueueNames(QUEUE_NAME);
container.setMessageListener(listenerAdapter);
return container;
}
@Bean
public MessageListenerAdapter listenerAdapter(MessageConsumer messageConsumer) {
return new MessageListenerAdapter(messageConsumer, "consumeStringMessage");
}
@Bean
public MessageConsumer eventReceiver(ObjectMapper objectMapper) {
return new MessageConsumer(objectMapper);
}
}

View File

@@ -1,16 +0,0 @@
package io.reflectoring;
import javax.validation.constraints.NotNull;
import lombok.Data;
@Data
public class User {
@NotNull
private long id;
@NotNull
private String name;
}

View File

@@ -1,16 +0,0 @@
package io.reflectoring;
import javax.validation.constraints.NotNull;
import lombok.Data;
@Data
public class UserCreatedMessage {
@NotNull
private String messageUuid;
@NotNull
private User user;
}

View File

@@ -1,59 +0,0 @@
package io.reflectoring;
import java.io.IOException;
import au.com.dius.pact.consumer.MessagePactBuilder;
import au.com.dius.pact.consumer.MessagePactProviderRule;
import au.com.dius.pact.consumer.Pact;
import au.com.dius.pact.consumer.PactVerification;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.model.v3.messaging.MessagePact;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class MessageConsumerTest {
@Rule
public MessagePactProviderRule mockProvider = new MessagePactProviderRule(this);
private byte[] currentMessage;
@Autowired
private MessageConsumer messageConsumer;
@Pact(provider = "userservice", consumer = "userclient")
public MessagePact userCreatedMessagePact(MessagePactBuilder builder) {
PactDslJsonBody body = new PactDslJsonBody();
body.stringType("messageUuid");
body.object("user")
.numberType("id", 42L)
.stringType("name", "Zaphod Beeblebrox")
.closeObject();
// @formatter:off
return builder
.expectsToReceive("a user created message")
.withContent(body)
.toPact();
// @formatter:on
}
@Test
@PactVerification("userCreatedMessagePact")
public void verifyCreatePersonPact() throws IOException {
messageConsumer.consumeStringMessage(new String(this.currentMessage));
}
/**
* This method is called by the Pact framework.
*/
public void setMessage(byte[] message) {
this.currentMessage = message;
}
}

View File

@@ -1,59 +0,0 @@
{
"consumer": {
"name": "userclient"
},
"provider": {
"name": "userservice"
},
"messages": [
{
"description": "a user created message",
"metaData": {
"Content-Type": "application/json; charset=UTF-8"
},
"contents": {
"messageUuid": "string",
"user": {
"id": 42,
"name": "Zaphod Beeblebrox"
}
},
"matchingRules": {
"body": {
"$.messageUuid": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.user.id": {
"matchers": [
{
"match": "number"
}
],
"combine": "AND"
},
"$.user.name": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.20"
}
}
}

View File

@@ -1,3 +0,0 @@
# Consumer-Driven-Contract Test for a Spring Boot Message Provider
This module shows how to use Pact to implement a contract test for a message provider.

View File

@@ -1,46 +0,0 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 11
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-amqp')
compile('com.h2database:h2:1.4.196')
compileOnly('org.projectlombok:lombok:1.18.2')
// add jaxb since it's no longer available in Java 11
runtime('javax.xml.bind:jaxb-api:2.3.1')
// add javassist >= 3.23.1-GA since earlier versions are broken in Java 11
// see https://github.com/jboss-javassist/javassist/issues/194
runtime('org.javassist:javassist:3.23.1-GA')
// overriding bytebuddy to newer version that supports Java 11
// see https://github.com/raphw/byte-buddy/issues/428
testCompile('net.bytebuddy:byte-buddy:1.9.12')
testCompile("au.com.dius:pact-jvm-provider-junit_2.12:${pact_version}")
testCompile('org.springframework.boot:spring-boot-starter-test')
}
bootRun {
jvmArgs = ["-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"]
}

View File

@@ -1,2 +0,0 @@
springboot_version=2.0.4.RELEASE
pact_version=3.5.20

View File

@@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip

View File

@@ -1,12 +0,0 @@
package io.reflectoring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -1,31 +0,0 @@
package io.reflectoring;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Takes a {@link UserCreatedMessage}, converts it to a {@link String} and sends it to be published.
*/
class MessageProducer {
private Logger logger = LoggerFactory.getLogger(MessageProducer.class);
private ObjectMapper objectMapper;
private MessagePublisher messagePublisher;
MessageProducer(ObjectMapper objectMapper, MessagePublisher messagePublisher) {
this.objectMapper = objectMapper;
this.messagePublisher = messagePublisher;
}
void produceUserCreatedMessage(UserCreatedMessage message) throws IOException {
String stringMessage = objectMapper.writeValueAsString(message);
messagePublisher.publishMessage(stringMessage, "user.created");
logger.info("Published message '{}'", stringMessage);
}
}

View File

@@ -1,36 +0,0 @@
package io.reflectoring;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
class MessageProviderConfiguration {
@Bean
TopicExchange topicExchange() {
return new TopicExchange("myExchange");
}
@Bean
MessageProducer messageProvider(ObjectMapper objectMapper, MessagePublisher publisher) {
return new MessageProducer(objectMapper, publisher);
}
@Bean
MessagePublisher messagePublisher(RabbitTemplate rabbitTemplate, TopicExchange topicExchange) {
return new MessagePublisher(rabbitTemplate, topicExchange);
}
@Bean
SendMessageJob job(MessageProducer messageProducer) {
return new SendMessageJob(messageProducer);
}
}

View File

@@ -1,24 +0,0 @@
package io.reflectoring;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
/**
* Publishes a String message to RabbitMQ.
*/
class MessagePublisher {
private RabbitTemplate rabbitTemplate;
private TopicExchange topicExchange;
MessagePublisher(RabbitTemplate rabbitTemplate, TopicExchange topicExchange) {
this.rabbitTemplate = rabbitTemplate;
this.topicExchange = topicExchange;
}
void publishMessage(String message, String routingKey) {
rabbitTemplate.convertAndSend(topicExchange.getName(), routingKey, message);
}
}

View File

@@ -1,37 +0,0 @@
package io.reflectoring;
import java.io.IOException;
import java.util.Random;
import java.util.UUID;
import org.springframework.scheduling.annotation.Scheduled;
class SendMessageJob {
private Random random = new Random();
private MessageProducer messageProducer;
SendMessageJob(MessageProducer messageProducer) {
this.messageProducer = messageProducer;
}
/**
* This scheduled job simulates the "real" business logic that should produce messages.
*/
@Scheduled(fixedDelay = 1000)
void sendUserCreatedMessage() {
try {
UserCreatedMessage userCreatedMessage = UserCreatedMessage.builder()
.messageUuid(UUID.randomUUID().toString())
.user(User.builder()
.id(random.nextLong())
.name("Zaphpod Beeblebrox")
.build())
.build();
messageProducer.produceUserCreatedMessage(userCreatedMessage);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

Some files were not shown because too many files have changed in this diff Show More