Compare commits

...

132 Commits

Author SHA1 Message Date
thombergs
34e0bfc156 argumentresolver example 2020-03-01 09:20:29 +11:00
thombergs
dfbc0446c9 MethodArgumentResolver example 2020-02-29 15:18:13 +11:00
Tom Hombergs
71d2a32f2f Merge pull request #14 from mukul-s/master
Reflect-76 ISP explained added code examples
2020-02-27 06:21:22 +11:00
thombergs
1e4e88c05e small changes to Main 2020-02-27 06:19:59 +11:00
Mukul Sharma
5fab70b931 Merge branch 'master' of https://github.com/mukul-s/code-examples 2020-02-26 01:55:03 +05:30
Mukul Sharma
3be25df928 Reflect-76 Added package 2020-02-26 01:54:18 +05:30
Mukul Sharma
f66aa05afb Merge branch 'master' of https://github.com/mukul-s/code-examples 2020-02-25 18:20:32 +05:30
Mukul Sharma
66d275d667 Merge branch 'master' of https://github.com/mukul-s/code-examples 2020-02-25 18:19:31 +05:30
Mukul Sharma
34211bfac9 Reflect 76 - Resolved merge conflicts 2020-02-25 18:18:53 +05:30
Mukul Sharma
1380621113 Resolved merge conflicts 2020-02-25 18:11:04 +05:30
Tom Hombergs
6e0af82475 Merge pull request #13 from petromir/master
Code examples for REFLECT-2 Contributor article
2020-02-21 06:24:57 +11:00
Tom Hombergs
7494ff7f89 Merge branch 'master' into master 2020-02-20 21:38:54 +11:00
thombergs
327e849575 included flyway module in build-all.sh 2020-02-20 21:38:00 +11:00
Petromir Dzhunev
a0ad81a332 REFLECT-2 Add missing bits
Add undo migration, add Jenkins files, add Java-base migration
2020-02-18 17:38:33 +02:00
thombergs
0be1335d24 add EXPOSE 2020-02-06 06:45:21 +11:00
thombergs
1437ac4429 added warning do code that isn't working 2020-02-03 06:56:10 +11:00
thombergs
5b76cbac62 reduces visibility 2020-02-03 06:37:25 +11:00
thombergs
8ca0e6e380 changes after review 2020-02-02 08:29:36 +11:00
thombergs
b6d167f4ae finished reactive example 2020-02-02 06:45:58 +11:00
Mukul Sharma
7934eed0ab Reflect-76 ISP explained added code examples 2020-02-01 04:13:37 +05:30
Tom Hombergs
ec2bdc9327 Reactive Batch Processing Example 2020-01-31 19:48:06 +11:00
Tom Hombergs
5fe5deca66 added Single.defer to fan out messages over multiple threads 2020-01-31 09:18:02 +11:00
Tom Hombergs
3fb809911a Reactive Batch Processing Example 2020-01-30 21:35:03 +11:00
Petromir Dzhunev
1809361301 REFLECT-2 Remove redundant parameters to Gradle plugin 2020-01-29 23:17:09 +02:00
Petromir Dzhunev
01828e504d REFLECT-2 Integrating H2 with Flyway 2020-01-24 22:35:17 +02:00
Tom Hombergs
3461549263 AWS Hello World docker file 2020-01-23 06:59:18 +11:00
Petromir Dzhunev
a2bb00faf1 Initial set-up of Flyway data migration project 2020-01-01 19:35:57 +02:00
Tom Hombergs
9e9b030c3f add README 2019-12-31 08:47:14 +11:00
Tom Hombergs
71af6ee95d add example for Spring Boot profiles 2019-12-31 08:40:49 +11:00
Tom Hombergs
b8e0845744 added missing gradle wrapper 2019-12-14 08:06:21 +11:00
Tom Hombergs
8baf275c8f isolated all modules 2019-12-14 07:59:28 +11:00
Tom Hombergs
92c266586c isolated some modules 2019-12-13 06:38:02 +11:00
Tom Hombergs
802046466f finish startup code example 2019-12-02 06:08:28 +11:00
Tom Hombergs
895d48c163 add startup examples 2019-11-30 07:46:18 +11:00
Tom Hombergs
6667bcde4d added @ConstructorBinding annotation 2019-11-17 06:33:05 +11:00
Tom Hombergs
5975aff396 modified code to match blog post 2019-11-17 06:25:58 +11:00
Tom Hombergs
44fb22ff50 fixed build 2019-11-10 10:29:35 +11:00
Tom Hombergs
dff7cf37f0 added missing gradle wrapper 2019-11-10 09:31:28 +11:00
Tom Hombergs
659a602e97 added code example for static data 2019-11-10 09:18:16 +11:00
Tom Hombergs
d34df7ad4e fixed build 2019-09-25 10:10:02 +02:00
Tom Hombergs
4c1318cc50 added immutable example code 2019-09-25 09:05:26 +02:00
Tom Hombergs
aa09ca31cb added mocking example 2019-09-18 08:03:33 +02:00
Tom Hombergs
7712f41e3e travis skip install 2019-09-03 06:44:50 +02:00
Tom Hombergs
2690f6b14d empty install, single call to each sub module in build-all.sh 2019-09-03 06:43:06 +02:00
Tom Hombergs
fa74e78e5d fixed travis build 2019-09-03 06:30:59 +02:00
Tom Hombergs
2c50fa9a12 fixed travis build 2019-09-03 06:01:01 +02:00
Tom Hombergs
c48921f554 fixed travis build 2019-09-03 05:46:55 +02:00
Tom Hombergs
df9575152f Merge remote-tracking branch 'origin/master' 2019-09-03 05:23:19 +02:00
Tom Hombergs
203c8d053a separated validation module from the rest 2019-09-03 05:23:07 +02:00
Tom Hombergs
9cf783c0a4 fixed URL 2019-08-30 21:14:09 +02:00
Tom Hombergs
9049c0f970 finalized code example for spring boot starter 2019-08-30 06:09:09 +02:00
Tom Hombergs
58c58e02d4 added permission for build.gradle 2019-08-28 21:38:13 +02:00
Tom Hombergs
9f84fcd49f added event-starter to travis build 2019-08-28 21:25:11 +02:00
Tom Hombergs
5646e0f169 added example Spring Boot Starter 2019-08-28 21:21:05 +02:00
Tom Hombergs
43e82f71d2 introduced BDDMockito 2019-07-01 21:39:09 +02:00
Tom Hombergs
8877f30ebc upgraded to Java 11 and fixed build 2019-03-30 08:02:10 +01:00
Tom Hombergs
b0ae545cd8 fixed compile error 2019-03-30 06:54:26 +01:00
Tom Hombergs
fb0aeb0a36 paging examples 2019-03-30 06:47:49 +01:00
Tom Hombergs
b3532e2f1e paging 2019-03-28 06:09:03 +01:00
Tom Hombergs
2c8d828ca1 pagination example 2019-03-25 15:35:32 +01:00
Tom Hombergs
d660463d74 added code examples for @ConfigurationProperties 2019-03-17 11:52:32 +01:00
Tom Hombergs
3207a3bc82 added README 2019-03-07 21:23:10 +01:00
Tom Hombergs
14039b84d5 added missing gradle files 2019-03-07 21:20:21 +01:00
Tom Hombergs
7528cd0069 code examples for Spring Boot @Conditionals 2019-03-07 06:44:14 +01:00
Tom Hombergs
f7e158a83c added missing flyway and liquibase scripts 2019-03-01 16:24:24 +01:00
Tom Hombergs
d01cccb574 refactored @SpringBootConfiguration examples 2019-03-01 11:52:52 +01:00
Tom Hombergs
71fad7e002 added code examples for @SpringBootTest article 2019-02-24 06:55:20 +01:00
Tom Hombergs
93f8074914 added missing test classes 2019-02-03 06:20:48 +01:00
Tom Hombergs
5ecb279f64 fixed build 2019-02-03 06:19:55 +01:00
Tom Hombergs
d5713b6ca3 code examples for @DataJpaTest 2019-02-03 06:15:17 +01:00
Tom Hombergs
188b53f526 moved jacocoTestReport to test instead of build phase 2019-01-31 21:00:14 +01:00
Tom Hombergs
86e8458980 minor changes 2019-01-21 06:36:50 +01:00
Tom Hombergs
8d8937c6bf tutorial about testing web controllers 2019-01-21 05:20:05 +01:00
Tom Hombergs
5352631522 Update README.md 2019-01-12 15:39:17 +01:00
Tom Hombergs
5f85a1cf67 added spring boot unit test example 2019-01-12 08:28:18 +01:00
Tom Hombergs
576a726cbd refactoring 2018-11-14 22:49:31 +01:00
Tom Hombergs
027abc1271 fixed node messaging example 2018-11-14 21:11:30 +01:00
Tom Hombergs
037dca0127 added .gitignore 2018-11-12 23:15:57 +01:00
Tom Hombergs
b245679a55 replaced broker config with local pact 2018-11-12 23:02:54 +01:00
Tom Hombergs
375b46309b added example for a node message provider 2018-11-12 23:00:33 +01:00
Tom Hombergs
38d9b4ee72 removed trailing comma 2018-11-11 21:56:28 +01:00
Tom Hombergs
93edb8c6f8 renamed test script 2018-11-11 21:55:48 +01:00
Tom Hombergs
a4c3f21391 node message consumer example 2018-11-11 21:55:18 +01:00
Tom Hombergs
382c307ae9 fixes for graphql consumer 2018-11-10 11:45:39 +01:00
Tom Hombergs
3e2c23cf45 added graphql provider test 2018-11-08 21:49:22 +01:00
Tom Hombergs
01242c5674 added missing setup script 2018-11-08 21:05:01 +01:00
Tom Hombergs
a58b5e670d added graphql consumer example 2018-11-08 16:31:46 +01:00
Tom Hombergs
a5a1381e1b added article link 2018-10-28 08:31:35 +01:00
Tom Hombergs
da30072399 re-created example repeating tutorial steps 2018-10-28 04:46:48 +01:00
Tom Hombergs
65c49a3ea7 added pact-node-provider example 2018-10-28 04:17:28 +01:00
Tom Hombergs
5fb2d69ca7 removed users endpoint 2018-10-27 06:02:57 +02:00
Tom Hombergs
a2df3dde0b basic heroes endpoint 2018-10-27 06:00:02 +02:00
Tom Hombergs
ff2030eafd basic express server 2018-10-27 05:47:23 +02:00
Tom Hombergs
4e45af2fbd basic express server 2018-10-27 05:47:06 +02:00
Tom Hombergs
8b384c602b updated README 2018-10-25 21:10:22 +02:00
Tom Hombergs
c11403d8fa added react pact example 2018-10-25 13:31:45 +02:00
Tom Hombergs
e8786b1bc3 ignored failing test due to complicated CI setup 2018-10-14 15:57:25 +02:00
Tom Hombergs
dab12b2c3c ignored failing test due to complicated CI setup 2018-10-14 15:37:33 +02:00
Tom Hombergs
31515b8a35 added missing classes 2018-10-14 15:07:06 +02:00
Tom Hombergs
aa05723b18 removed local jdk11 reference 2018-10-14 14:58:54 +02:00
Tom Hombergs
cac0c62b71 fixed jacoco build 2018-10-14 14:54:50 +02:00
Tom Hombergs
24d00fae37 Spring Boot Bean Validation example 2018-10-14 14:48:57 +02:00
Tom Hombergs
03f1680103 added jacoco code example 2018-10-05 21:06:13 +02:00
Tom Hombergs
884546b69c added dependency to spring-cloud-contract-provider 2018-10-02 06:42:40 +02:00
Tom Hombergs
be8be2efc4 added --info to travis build 2018-10-02 06:21:48 +02:00
Tom Hombergs
0e2f10862e changed port 2018-10-02 06:11:47 +02:00
Tom Hombergs
441cd4f801 updated gradle wrapper and activated junit 5 tests in gradle build 2018-09-15 12:08:15 +02:00
Tom Hombergs
de7fa53d55 refactored message pact examples 2018-09-13 06:24:27 +02:00
Tom Hombergs
b4bec09a3e updated readmes 2018-09-10 21:45:22 +02:00
Tom Hombergs
818e162c7f Merge branch 'pact-message-provider' 2018-09-10 21:42:04 +02:00
Tom Hombergs
a18ad84da2 working (but awkward) example of a message provider test 2018-09-10 21:41:49 +02:00
Tom Hombergs
d60f95b6c0 added readme with link to article 2018-08-31 09:00:24 +02:00
Tom Hombergs
52bc347d12 added object mother with fluent builder example 2018-08-31 08:59:04 +02:00
Tom Hombergs
5e93dcb84b updated messaging example for cleaner separation of publishing and converting messages 2018-08-26 22:58:14 +02:00
Tom Hombergs
3b0eb77b5d example for message provider with pact (not working yet) 2018-08-24 11:16:30 +02:00
Tom Hombergs
a9c9faba1d added example for a message consumer test with pact 2018-08-22 21:32:54 +02:00
Tom Hombergs
8672d163e2 updated spring-cloud-contract exampled to current versions 2018-08-16 22:27:41 +02:00
Tom Hombergs
3eefd80192 removed the manual truncating 2018-08-11 21:35:37 +02:00
Tom Hombergs
5a36a2365e updated pact provider example to JUnit 5 and Spring Boot 2 2018-08-11 16:47:03 +02:00
Tom Hombergs
5f675f91a8 changed logging configuration 2018-08-11 00:22:08 +02:00
Tom Hombergs
3559378d1a logging example with Spring Boot and Logback 2018-08-11 00:00:15 +02:00
Tom Hombergs
b72ab0d580 Merge pull request #8 from thombergs/cdc-updates
updated pact-feign example to Spring Boot 2 and JUnit 5
2018-08-10 00:14:21 +02:00
Tom Hombergs
ed45066496 updated pact-feign example to Spring Boot 2 and JUnit 5 2018-08-10 00:09:11 +02:00
Tom Hombergs
ddb3e08ee5 Merge pull request #7 from thombergs/logging-format
removed deprecated examples
2018-08-04 09:21:55 +02:00
Tom Hombergs
888a89e541 removed deprecated examples
added logging example
2018-08-04 09:03:43 +02:00
Tom Hombergs
fb148fcd14 added article link 2018-06-11 22:33:27 +02:00
Tom Hombergs
a07407dc9f changed CI config to skip build on docs-only-changes 2018-06-11 22:15:15 +02:00
Tom Hombergs
5e4c962b21 Merge pull request #6 from thombergs/spring-boot-testing
Spring boot testing
2018-05-27 22:31:06 +02:00
Tom Hombergs
500ee0a04f ignored test because it cannot run on CI 2018-05-27 21:55:26 +02:00
Tom Hombergs
4d67dd44d0 fix 2018-05-27 21:10:15 +02:00
Tom Hombergs
a4d70d5835 Example application for structuring a Spring Boot app 2018-05-27 20:55:24 +02:00
Tom Hombergs
f2b83425ac inital checking for spring-boot-test example project 2018-05-20 20:38:28 +02:00
623 changed files with 37502 additions and 1310 deletions

3
.gitignore vendored Normal file
View File

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

View File

@@ -1,7 +1,18 @@
before_install:
- chmod +x gradlew
- 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
language: java
jdk:
- oraclejdk8
- oraclejdk11

View File

@@ -2,7 +2,20 @@
[![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 java technologies.
This repo contains example projects which show how to use different (not only) 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.
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.

32
aws/aws-hello-world/.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
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

@@ -0,0 +1,5 @@
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

@@ -0,0 +1,24 @@
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()
}

Binary file not shown.

View File

@@ -1,5 +1,5 @@
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-bin.zip

172
aws/aws-hello-world/gradlew vendored Executable file
View File

@@ -0,0 +1,172 @@
#!/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

View File

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

View File

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,14 @@
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

@@ -0,0 +1 @@

View File

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

70
build-all.sh Normal file
View File

@@ -0,0 +1,70 @@
#!/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
}
}
chmod +x gradlew
build_gradle_module "spring-boot/argumentresolver"
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-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

@@ -1,15 +0,0 @@
# 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

@@ -1,46 +0,0 @@
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,24 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,15 +0,0 @@
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

@@ -1,17 +0,0 @@
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

@@ -1,137 +0,0 @@
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,16 +0,0 @@
# 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

@@ -1,35 +0,0 @@
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,43 +0,0 @@
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

@@ -1,14 +0,0 @@
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

@@ -1,30 +0,0 @@
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

@@ -1,9 +0,0 @@
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,7 +0,0 @@
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

@@ -1,41 +0,0 @@
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

@@ -1,32 +0,0 @@
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

@@ -1,36 +0,0 @@
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

@@ -1,50 +0,0 @@
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

@@ -1,70 +0,0 @@
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

@@ -1,64 +0,0 @@
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

@@ -1,111 +0,0 @@
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

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

View File

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

View File

@@ -1,6 +0,0 @@
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

@@ -1,11 +0,0 @@
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

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

View File

@@ -7,7 +7,7 @@ buildscript {
apply plugin: 'java'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
sourceCompatibility = 11
repositories {
mavenLocal()
@@ -23,3 +23,6 @@ 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-4.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip

7
logging/README.md Normal file
View File

@@ -0,0 +1,7 @@
# 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/)

20
logging/build.gradle Normal file
View File

@@ -0,0 +1,20 @@
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,6 +1,5 @@
#Sun Jul 30 16:58:54 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip

View File

@@ -0,0 +1,18 @@
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

@@ -0,0 +1,19 @@
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

@@ -0,0 +1,21 @@
<?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,6 +4,9 @@ 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,5 +0,0 @@
userservice:
ribbon:
eureka:
enabled: false
listOfServers: localhost:8080

View File

@@ -1,5 +1,3 @@
apply plugin: 'org.springframework.boot'
buildscript {
repositories {
mavenLocal()
@@ -10,18 +8,56 @@ buildscript {
}
}
plugins {
id "au.com.dius.pact" version "3.5.20"
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
version '1.0.0.SNAPSHOT'
repositories {
mavenLocal()
jcenter()
}
dependencies {
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')
testCompile('org.codehaus.groovy:groovy-all:2.4.6')
testCompile("au.com.dius:pact-jvm-consumer-junit_2.11:3.5.2")
testCompile("org.springframework.boot:spring-boot-starter-test:${springboot_version}")
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('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')
}
pact {
publish {
pactDirectory = 'target/pacts'
pactBrokerUrl = 'TODO'
pactBrokerUsername = 'TODO'
pactBrokerPassword = 'TODO'
}
}
test {
useJUnitPlatform()
}

View File

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

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.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip

View File

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

View File

@@ -0,0 +1,15 @@
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.netflix.feign.FeignClient;
import org.springframework.cloud.openfeign.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,83 +1,88 @@
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.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.client.RestTemplate;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
// overriding provider address
"userservice.ribbon.listOfServers: localhost:8888"
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"
})
public class UserServiceConsumerTest {
class UserServiceConsumerTest {
@Rule
public PactProviderRuleMk2 stubProvider = new PactProviderRuleMk2("userservice", "localhost", 8888, this);
@Autowired
private UserClient userClient;
@Autowired
private UserClient userClient;
@Pact(state = "provider accepts a new person", provider = "userservice", consumer = "userclient")
RequestResponsePact createPersonPact(PactDslWithProvider builder) {
@Pact(state = "provider accepts a new person", 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();
}
// @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(state = "person 42 exists", 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();
}
@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
}
@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 = "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 = "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");
}
@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");
}
}

View File

@@ -0,0 +1,115 @@
{
"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

@@ -0,0 +1,7 @@
# 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

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

View File

@@ -0,0 +1,43 @@
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

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

View File

@@ -0,0 +1,5 @@
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

View File

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,37 @@
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

@@ -0,0 +1,60 @@
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

@@ -0,0 +1,16 @@
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

@@ -0,0 +1,16 @@
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

@@ -0,0 +1,59 @@
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

@@ -0,0 +1,59 @@
{
"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

@@ -0,0 +1,3 @@
# 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

@@ -0,0 +1,46 @@
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

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

View File

@@ -0,0 +1,5 @@
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

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,31 @@
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

@@ -0,0 +1,36 @@
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

@@ -0,0 +1,24 @@
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

@@ -0,0 +1,37 @@
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);
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
package io.reflectoring;
import java.util.List;
import au.com.dius.pact.model.Interaction;
import au.com.dius.pact.model.ProviderState;
import au.com.dius.pact.provider.ConsumerInfo;
import au.com.dius.pact.provider.ProviderInfo;
import au.com.dius.pact.provider.ProviderVerifier;
import au.com.dius.pact.provider.junit.target.AmqpTarget;
import org.jetbrains.annotations.NotNull;
/**
* Custom implementation of {@link AmqpTarget} since with {@link AmqpTarget}
* I had classpath troubles (see https://github.com/DiUS/pact-jvm/issues/763).
* Use at own risk, since the implementation is not complete and may behave
* a little different that the original class.
*/
public class CustomAmqpTarget extends AmqpTarget {
public CustomAmqpTarget(List<String> packagesToScan) {
super(packagesToScan);
}
@NotNull
@Override
protected ProviderVerifier setupVerifier(Interaction interaction, ProviderInfo provider, ConsumerInfo consumer) {
ProviderVerifier verifier = new CustomProviderVerifier(getPackagesToScan());
setupReporters(verifier, provider.getName(), interaction.getDescription());
verifier.initialiseReporters(provider);
verifier.reportVerificationForConsumer(consumer, provider);
if (!interaction.getProviderStates().isEmpty()) {
for (ProviderState state : interaction.getProviderStates()) {
verifier.reportStateForInteraction(state.getName(), provider, consumer, true);
}
}
verifier.reportInteractionDescription(interaction);
return verifier;
}
}

View File

@@ -0,0 +1,62 @@
package io.reflectoring;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import au.com.dius.pact.model.Interaction;
import au.com.dius.pact.model.v3.messaging.Message;
import au.com.dius.pact.provider.ConsumerInfo;
import au.com.dius.pact.provider.PactVerifyProvider;
import au.com.dius.pact.provider.ProviderInfo;
import au.com.dius.pact.provider.ProviderVerifier;
import org.reflections.Reflections;
import org.reflections.scanners.MethodAnnotationsScanner;
import org.reflections.util.ConfigurationBuilder;
public class CustomProviderVerifier extends ProviderVerifier {
private List<String> packagesToScan;
public CustomProviderVerifier(List<String> packagesToScan) {
this.packagesToScan = packagesToScan;
}
@Override
public boolean verifyResponseByInvokingProviderMethods(ProviderInfo providerInfo, ConsumerInfo consumer,
Object interaction, String interactionMessage, Map failures) {
try {
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
.setScanners(new MethodAnnotationsScanner())
.forPackages(packagesToScan.toArray(new String[]{}));
Reflections reflections = new Reflections(configurationBuilder);
Set<Method> methodsAnnotatedWith = reflections.getMethodsAnnotatedWith(PactVerifyProvider.class);
Set<Method> providerMethods = methodsAnnotatedWith.stream()
.filter(m -> {
PactVerifyProvider annotation = m.getAnnotation(PactVerifyProvider.class);
return annotation.value().equals(((Interaction)interaction).getDescription());
})
.collect(Collectors.toSet());
if (providerMethods.isEmpty()) {
throw new RuntimeException("No annotated methods were found for interaction " +
"'${interaction.description}'. You need to provide a method annotated with " +
"@PactVerifyProvider(\"${interaction.description}\") that returns the message contents.");
} else {
if (interaction instanceof Message) {
verifyMessagePact(providerMethods, (Message) interaction, interactionMessage, failures);
} else {
throw new RuntimeException("only supports Message interactions!");
}
}
} catch (Exception e) {
throw new RuntimeException("verification failed", e);
}
return true;
}
}

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