├── .github
└── workflows
│ └── gradle.yml
├── .gitignore
├── README.md
├── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
├── src
├── integration
│ └── java
│ │ └── com
│ │ └── demo
│ │ ├── AppRestAssuredIT.java
│ │ └── AppTestRestTemplateIT.java
├── main
│ ├── java
│ │ └── com
│ │ │ └── demo
│ │ │ ├── SpringTestApplication.java
│ │ │ ├── config
│ │ │ └── SwaggerConfig.java
│ │ │ ├── controllers
│ │ │ └── VehicleController.java
│ │ │ ├── dto
│ │ │ └── Vehicle.java
│ │ │ ├── exceptions
│ │ │ ├── ApiErrorMessage.java
│ │ │ ├── VehicleExceptionHandler.java
│ │ │ └── VehicleNotFoundException.java
│ │ │ ├── repository
│ │ │ └── VehicleRepository.java
│ │ │ └── services
│ │ │ ├── VehicleService.java
│ │ │ └── VehicleServiceImpl.java
│ └── resources
│ │ ├── application-integration.properties
│ │ ├── application-test.properties
│ │ ├── application.properties
│ │ └── data-h2.sql
└── test
│ └── java
│ └── com
│ └── demo
│ └── DemoWebLayerTest.java
└── structure.PNG
/.github/workflows/gradle.yml:
--------------------------------------------------------------------------------
1 | name: Java CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Set up JDK 1.8
13 | uses: actions/setup-java@v1
14 | with:
15 | java-version: 1.8
16 | - name: Build with Gradle
17 | run: ./gradlew build
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**
6 | !**/src/test/**
7 |
8 | ### STS ###
9 | .apt_generated
10 | .classpath
11 | .factorypath
12 | .project
13 | .settings
14 | .springBeans
15 | .sts4-cache
16 |
17 | ### IntelliJ IDEA ###
18 | .idea
19 | *.iws
20 | *.iml
21 | *.ipr
22 | out/
23 |
24 | ### NetBeans ###
25 | /nbproject/private/
26 | /nbbuild/
27 | /dist/
28 | /nbdist/
29 | /.nb-gradle/
30 |
31 | ### VS Code ###
32 | .vscode/
33 |
34 | ### Gradle ###
35 | bin
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # spring-demo
2 | Spring boot demo for web layer and integration testing
3 |
4 | For a full demo explanation:
5 |
6 | Testing Spring Boot RESTful APIs using MockMvc/Mockito, Test RestTemplate and RestAssured
7 |
8 |
9 | 
10 |
11 | To build Gradle project from the source directory use:
12 | `./gradlew clean build --info`
13 |
14 | To run unit tests:
15 | `./gradlew clean test --info`
16 |
17 | To run integration tests:
18 | `./gradlew clean integration --info`
19 |
20 | To run the application:
21 | `./gradlew bootRun` it will start on the default 8080 port
22 |
23 | Are you up?? - http://localhost:8080/actuator/health
24 |
25 | Wanna look at some tables?? - http://localhost:8080/h2-console/
26 | User=admin Password=admin
27 |
28 |
29 | Got Swag?? - http://localhost:8080/swagger-ui.html
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'org.springframework.boot' version '2.2.0.RELEASE'
3 | id 'io.spring.dependency-management' version '1.0.8.RELEASE'
4 | id 'java'
5 | }
6 |
7 | group = 'com.example'
8 | version = '0.0.1-SNAPSHOT'
9 | sourceCompatibility = '1.8'
10 |
11 | configurations {
12 | developmentOnly
13 | runtimeClasspath {
14 | extendsFrom developmentOnly
15 | }
16 | compileOnly {
17 | extendsFrom annotationProcessor
18 | }
19 | integrationImplementation.extendsFrom testImplementation
20 | integrationRuntimeOnly.extendsFrom testRuntimeOnly
21 | }
22 |
23 | repositories {
24 | mavenCentral()
25 | }
26 |
27 | dependencies {
28 | implementation 'org.springframework.boot:spring-boot-starter-actuator'
29 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
30 | implementation 'org.springframework.boot:spring-boot-starter-web'
31 |
32 | developmentOnly 'org.springframework.boot:spring-boot-devtools'
33 |
34 | runtimeOnly 'com.h2database:h2'
35 |
36 | compile "io.springfox:springfox-swagger2:2.9.2"
37 | compile "io.springfox:springfox-swagger-ui:2.9.2"
38 |
39 | compileOnly 'org.projectlombok:lombok'
40 | annotationProcessor 'org.projectlombok:lombok'
41 |
42 | testImplementation 'io.rest-assured:rest-assured:3.3.0'
43 | testImplementation 'io.rest-assured:json-schema-validator:3.0.6'
44 |
45 | testImplementation('org.springframework.boot:spring-boot-starter-test') {
46 | exclude(group: 'junit', module: 'junit')
47 | }
48 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1'
49 | testImplementation 'org.mockito:mockito-junit-jupiter:2.28.2'
50 |
51 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1'
52 | testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.3.1'
53 | }
54 |
55 | sourceSets {
56 |
57 | test {
58 | java.srcDirs = ['src/test/java']
59 | resources.srcDirs = ['src/test/resources']
60 | }
61 |
62 | integration {
63 | java.srcDirs = ['src/integration/java']
64 | resources.srcDirs = ['src/integration/resources']
65 | compileClasspath += sourceSets.main.output + sourceSets.test.output
66 | runtimeClasspath += sourceSets.main.output + sourceSets.test.output
67 | }
68 | }
69 |
70 | task integration(type:Test, description: 'Integration testing of API', group:'Verification') {
71 | testClassesDirs = sourceSets.integration.output.classesDirs
72 | classpath = sourceSets.integration.runtimeClasspath
73 | outputs.upToDateWhen {false}
74 | useJUnitPlatform(){
75 | failFast = true
76 | }
77 | }
78 |
79 | test {
80 | maxHeapSize = '1024m'
81 | useJUnitPlatform()
82 | }
83 |
84 | build.dependsOn integration
85 | integration.mustRunAfter test
86 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jet-C/spring-demo/4fc55d3d035e9df6249df83e4b2bb4f0f69a3c6f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 | # Determine the Java command to use to start the JVM.
86 | if [ -n "$JAVA_HOME" ] ; then
87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
88 | # IBM's JDK on AIX uses strange locations for the executables
89 | JAVACMD="$JAVA_HOME/jre/sh/java"
90 | else
91 | JAVACMD="$JAVA_HOME/bin/java"
92 | fi
93 | if [ ! -x "$JAVACMD" ] ; then
94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
95 |
96 | Please set the JAVA_HOME variable in your environment to match the
97 | location of your Java installation."
98 | fi
99 | else
100 | JAVACMD="java"
101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
102 |
103 | Please set the JAVA_HOME variable in your environment to match the
104 | location of your Java installation."
105 | fi
106 |
107 | # Increase the maximum file descriptors if we can.
108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
109 | MAX_FD_LIMIT=`ulimit -H -n`
110 | if [ $? -eq 0 ] ; then
111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
112 | MAX_FD="$MAX_FD_LIMIT"
113 | fi
114 | ulimit -n $MAX_FD
115 | if [ $? -ne 0 ] ; then
116 | warn "Could not set maximum file descriptor limit: $MAX_FD"
117 | fi
118 | else
119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
120 | fi
121 | fi
122 |
123 | # For Darwin, add options to specify how the application appears in the dock
124 | if $darwin; then
125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
126 | fi
127 |
128 | # For Cygwin or MSYS, switch paths to Windows format before running java
129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
132 | JAVACMD=`cygpath --unix "$JAVACMD"`
133 |
134 | # We build the pattern for arguments to be converted via cygpath
135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
136 | SEP=""
137 | for dir in $ROOTDIRSRAW ; do
138 | ROOTDIRS="$ROOTDIRS$SEP$dir"
139 | SEP="|"
140 | done
141 | OURCYGPATTERN="(^($ROOTDIRS))"
142 | # Add a user-defined pattern to the cygpath arguments
143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
145 | fi
146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
147 | i=0
148 | for arg in "$@" ; do
149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
151 |
152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
154 | else
155 | eval `echo args$i`="\"$arg\""
156 | fi
157 | i=$((i+1))
158 | done
159 | case $i in
160 | (0) set -- ;;
161 | (1) set -- "$args0" ;;
162 | (2) set -- "$args0" "$args1" ;;
163 | (3) set -- "$args0" "$args1" "$args2" ;;
164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
170 | esac
171 | fi
172 |
173 | # Escape application args
174 | save () {
175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
176 | echo " "
177 | }
178 | APP_ARGS=$(save "$@")
179 |
180 | # Collect all arguments for the java command, following the shell quoting and substitution rules
181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
182 |
183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
185 | cd "$(dirname "$0")"
186 | fi
187 |
188 | exec "$JAVACMD" "$@"
189 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
34 |
35 | @rem Find java.exe
36 | if defined JAVA_HOME goto findJavaFromJavaHome
37 |
38 | set JAVA_EXE=java.exe
39 | %JAVA_EXE% -version >NUL 2>&1
40 | if "%ERRORLEVEL%" == "0" goto init
41 |
42 | echo.
43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
44 | echo.
45 | echo Please set the JAVA_HOME variable in your environment to match the
46 | echo location of your Java installation.
47 |
48 | goto fail
49 |
50 | :findJavaFromJavaHome
51 | set JAVA_HOME=%JAVA_HOME:"=%
52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
53 |
54 | if exist "%JAVA_EXE%" goto init
55 |
56 | echo.
57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
58 | echo.
59 | echo Please set the JAVA_HOME variable in your environment to match the
60 | echo location of your Java installation.
61 |
62 | goto fail
63 |
64 | :init
65 | @rem Get command-line arguments, handling Windows variants
66 |
67 | if not "%OS%" == "Windows_NT" goto win9xME_args
68 |
69 | :win9xME_args
70 | @rem Slurp the command line arguments.
71 | set CMD_LINE_ARGS=
72 | set _SKIP=2
73 |
74 | :win9xME_args_slurp
75 | if "x%~1" == "x" goto execute
76 |
77 | set CMD_LINE_ARGS=%*
78 |
79 | :execute
80 | @rem Setup the command line
81 |
82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
83 |
84 | @rem Execute Gradle
85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
86 |
87 | :end
88 | @rem End local scope for the variables with windows NT shell
89 | if "%ERRORLEVEL%"=="0" goto mainEnd
90 |
91 | :fail
92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
93 | rem the _cmd.exe /c_ return code!
94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
95 | exit /b 1
96 |
97 | :mainEnd
98 | if "%OS%"=="Windows_NT" endlocal
99 |
100 | :omega
101 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'demo'
2 |
--------------------------------------------------------------------------------
/src/integration/java/com/demo/AppRestAssuredIT.java:
--------------------------------------------------------------------------------
1 | package com.demo;
2 |
3 | import org.junit.jupiter.api.BeforeAll;
4 | import org.junit.jupiter.api.Test;
5 | import org.junit.jupiter.api.TestInstance;
6 | import org.junit.jupiter.api.TestInstance.Lifecycle;
7 | import org.springframework.beans.factory.annotation.Value;
8 | import org.springframework.boot.test.context.SpringBootTest;
9 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
10 | import org.springframework.http.HttpStatus;
11 | import org.springframework.http.MediaType;
12 | import org.springframework.test.context.ActiveProfiles;
13 | import com.demo.dto.Vehicle;
14 |
15 | import io.restassured.response.ValidatableResponse;
16 | import static io.restassured.RestAssured.*;
17 | import static org.hamcrest.Matchers.*;
18 |
19 | /*
20 | * @SpringBootTest - Run our app in as a test context enabling @Test methods.
21 | * @ActiveProfiles - Select profile configurations. We will use the (application-'integration'.properties)
22 | * @TestInstance - Enable @BeforeAll and share same instance of class for every test
23 | */
24 | @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = SpringTestApplication.class)
25 | @TestInstance(Lifecycle.PER_CLASS)
26 | @ActiveProfiles({ "integration" })
27 | public class AppRestAssuredIT {
28 |
29 | @Value("${local.server.port}")
30 | private int ports;
31 |
32 | @BeforeAll
33 | public void setUp() {
34 | port = ports;
35 | baseURI = "http://localhost/demo"; // Will result in "http://localhost:xxxx/demo"
36 | }
37 |
38 | @Test
39 | public void get_AllVehicles_returnsAllVehicles_200() {
40 |
41 | String vinArray[] = { "FR45212A24D4SED66", "FR4EDED2150RFT5GE", "XDFR6545DF3A5R896", "GMDE65A5ED66ER002",
42 | "PQERS2A36458E98CD", "194678S400005", "48955460210" };
43 |
44 | ValidatableResponse response = given().contentType(MediaType.APPLICATION_JSON_VALUE)
45 | .accept(MediaType.APPLICATION_JSON_VALUE).when().get("/vehicles").then();
46 |
47 | System.out.println("'getAllVehicles_returnsAllVehicles_200()' response:\n" + response.extract().asString());
48 |
49 | response.assertThat().statusCode(HttpStatus.OK.value()).body("content.size()", greaterThan(7))
50 | .body(containsString("Corvette")).body("find {it.vin == 'FR45212A24D4SED66'}.year", equalTo(2010))
51 | .body("find {it.vin == 'FR45212A24D4SED66'}.year", equalTo(2010))
52 | .body("make", hasItems("Ford", "Chevrolet", "Toyota", "Nissan")).body("make", not(hasItem("Honda")))
53 | .body("vin", hasItems(vinArray)).body("findAll {it.year < 1990}.size()", is(2));
54 | }
55 |
56 | @Test
57 | public void get_VehicleById_returnsVehicle_200() {
58 |
59 | ValidatableResponse response = given().contentType(MediaType.APPLICATION_JSON_VALUE)
60 | .accept(MediaType.APPLICATION_JSON_VALUE).when().get("/vehicles/FR45212A24D4SED66").then();
61 |
62 | System.out.println("'getVehicleById_returnsVehicle_200()' response:\n" + response.extract().asString());
63 |
64 | response.assertThat().statusCode(HttpStatus.OK.value()).body("make", equalTo("Ford"))
65 | .body("model", equalTo("F-150")).body("year", equalTo(2010)).body("is_older", equalTo(false));
66 | }
67 |
68 | @Test
69 | public void get_VehicleById_returnsNotFound_404() {
70 |
71 | ValidatableResponse response = given().contentType(MediaType.APPLICATION_JSON_VALUE)
72 | .accept(MediaType.APPLICATION_JSON_VALUE).when().get("/vehicles/NON-EXISTING-ID77").then();
73 |
74 | System.out.println("'get_VehicleById_returnsNotFound_404()' response:\n" + response.extract().asString());
75 |
76 | response.assertThat().statusCode(HttpStatus.NOT_FOUND.value()).body("errorMessage",
77 | containsString("404 Vehicle with VIN (NON-EXISTING-ID77) not found"));
78 | }
79 |
80 | @Test
81 | public void post_newVehicle_returnsCreatedVehicle_201() {
82 | // Build new vehicle to post
83 | Vehicle newVehicle = Vehicle.builder().vin("X0RF654S54A65E66E").make("Toyota").model("Supra").year(2020)
84 | .is_older(false).build();
85 |
86 | ValidatableResponse response = given().contentType(MediaType.APPLICATION_JSON_VALUE)
87 | .accept(MediaType.APPLICATION_JSON_VALUE).body(newVehicle).when().post("/create/vehicle").then();
88 |
89 | System.out.println("'post_Vehicle_returnsCreatedVehicle_201()' response:\n" + response.extract().asString());
90 |
91 | response.assertThat().statusCode(HttpStatus.CREATED.value()).body("vin", equalTo("X0RF654S54A65E66E"))
92 | .body("make", equalTo("Toyota")).body("model", equalTo("Supra")).body("year", equalTo(2020))
93 | .body("is_older", equalTo(false));
94 | }
95 |
96 | @Test
97 | public void post_newVehicle_Returns_BadRequest_400() {
98 | // Create new vehicle with a bad VIN length for the declared model year
99 | Vehicle newVehicle = Vehicle.builder().vin("BAD-LENGTH-VIN").make("Chevrolet").model("Camaro").year(2018)
100 | .is_older(false).build();
101 |
102 | ValidatableResponse response = given().contentType(MediaType.APPLICATION_JSON_VALUE)
103 | .accept(MediaType.APPLICATION_JSON_VALUE).body(newVehicle).when().post("/create/vehicle").then();
104 |
105 | System.out.println("'post_newVehicle_Returns_BadRequest_400()' response:\n" + response.extract().asString());
106 |
107 | response.assertThat().statusCode(HttpStatus.BAD_REQUEST.value()).body("errorMessage",
108 | containsString("VIN length is invalid for the declared year"));
109 | }
110 |
111 | @Test
112 | public void put_updateVehicle_returnsUpdatedVehicle_202() {
113 | // Update the year on the vehicle 1992 -> 1997
114 | Vehicle updateVehicle = Vehicle.builder().vin("FR4EDED2150RFT5GE").make("Ford").model("Ranger").year(1997)
115 | .is_older(false).build();
116 |
117 | ValidatableResponse response = given().contentType(MediaType.APPLICATION_JSON_VALUE)
118 | .accept(MediaType.APPLICATION_JSON_VALUE).body(updateVehicle).when()
119 | .put("/update/vehicle/FR4EDED2150RFT5GE").then();
120 |
121 | System.out
122 | .println("'put_updateVehicle_returnsUpdatedVehicle_202()' response:\n" + response.extract().asString());
123 |
124 | response.assertThat().statusCode(HttpStatus.ACCEPTED.value()).body("vin", equalTo("FR4EDED2150RFT5GE"))
125 | .body("make", equalTo("Ford")).body("model", equalTo("Ranger")).body("year", equalTo(1997))
126 | .body("is_older", equalTo(false));
127 | }
128 |
129 | @Test
130 | public void delete_vehicle_returnsNoContent_204() {
131 |
132 | ValidatableResponse response = given().contentType(MediaType.APPLICATION_JSON_VALUE)
133 | .accept(MediaType.APPLICATION_JSON_VALUE).when().delete("/vehicles/XDFR64AE9F3A5R78S").then();
134 |
135 | System.out.println("'delete_vehicle_returnsNoContent_204()' response:\n" + response.extract().asString());
136 |
137 | response.assertThat().statusCode(HttpStatus.NO_CONTENT.value());
138 | }
139 |
140 | }
141 |
--------------------------------------------------------------------------------
/src/integration/java/com/demo/AppTestRestTemplateIT.java:
--------------------------------------------------------------------------------
1 | package com.demo;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertTrue;
4 | import static org.junit.jupiter.api.Assertions.assertFalse;
5 | import static org.junit.jupiter.api.Assertions.assertNull;
6 | import static org.junit.jupiter.api.Assertions.assertEquals;
7 |
8 | import java.util.List;
9 | import java.util.Optional;
10 | import java.util.stream.Collectors;
11 | import java.util.stream.Stream;
12 |
13 | import org.junit.jupiter.api.Test;
14 | import org.springframework.beans.factory.annotation.Autowired;
15 | import org.springframework.boot.test.context.SpringBootTest;
16 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
17 | import org.springframework.boot.test.web.client.TestRestTemplate;
18 | import org.springframework.core.ParameterizedTypeReference;
19 | import org.springframework.http.HttpEntity;
20 | import org.springframework.http.HttpHeaders;
21 | import org.springframework.http.HttpMethod;
22 | import org.springframework.http.HttpStatus;
23 | import org.springframework.http.MediaType;
24 | import org.springframework.http.ResponseEntity;
25 | import org.springframework.test.context.ActiveProfiles;
26 | import com.demo.dto.Vehicle;
27 | import com.demo.repository.VehicleRepository;
28 | import com.fasterxml.jackson.core.JsonProcessingException;
29 | import com.fasterxml.jackson.databind.JsonNode;
30 | import com.fasterxml.jackson.databind.ObjectMapper;
31 |
32 | /*
33 | * @SpringBootTest - Run our app in as a test context enabling @Test methods.
34 | * @ActiveProfiles - Select profile configurations. We will use the (application-'integration'.properties)
35 | */
36 | @SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT, classes = SpringTestApplication.class)
37 | @ActiveProfiles("integration")
38 | public class AppTestRestTemplateIT {
39 |
40 | final private static int port = 8080;
41 | final private static String baseUrl = "http://localhost:";
42 |
43 | /*
44 | * @SpringBootTest registers a TestRestTeplate bean so we can directly @Autowire
45 | * it in
46 | */
47 | @Autowired
48 | private TestRestTemplate restTemplate;
49 |
50 | @Autowired
51 | private VehicleRepository vehicleRepository;
52 |
53 | @Test
54 | public void get_allVehicles_ReturnsAllVehicles_OK() {
55 |
56 | List expectedVINList = Stream.of("FR45212A24D4SED66", "FR4EDED2150RFT5GE", "XDFR6545DF3A5R896",
57 | "XDFR64AE9F3A5R78S", "PQERS2A36458E98CD", "194678S400005", "48955460210").collect(Collectors.toList());
58 |
59 | ResponseEntity> responseEntity = this.restTemplate.exchange(baseUrl + port + "/demo/vehicles",
60 | HttpMethod.GET, null, new ParameterizedTypeReference>() {
61 | });
62 |
63 | List vehiclesResponseList = responseEntity.getBody();
64 |
65 | assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
66 | assertTrue(vehiclesResponseList.size() > 7);
67 | assertTrue(vehiclesResponseList.stream().anyMatch((vehicle) -> {
68 | return expectedVINList.contains(vehicle.getVin());
69 | }));
70 | }
71 |
72 | @Test
73 | public void get_vehicleById_Returns_Vehicle_OK() {
74 |
75 | ResponseEntity responseEntity = this.restTemplate
76 | .getForEntity(baseUrl + port + "/demo/vehicles/48955460210", Vehicle.class);
77 |
78 | Vehicle expectedVehicle = Vehicle.builder().vin("48955460210").make("Ford").model("Mustang").year(1974)
79 | .is_older(true).build();
80 |
81 | assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
82 | assertEquals(expectedVehicle, responseEntity.getBody());
83 | }
84 |
85 | @Test
86 | public void get_vehicleById_Returns_NotFound_404() {
87 |
88 | // We are expecting an string error message in JSON
89 | ResponseEntity result = this.restTemplate.exchange(baseUrl + port + "/demo/vehicles/MISSING-VIN123456",
90 | HttpMethod.GET, null, String.class);
91 |
92 | // Parse JSON message response
93 | ObjectMapper mapper = new ObjectMapper();
94 | JsonNode jsonTree = null;
95 | try {
96 | jsonTree = mapper.readTree(result.getBody());
97 | } catch (JsonProcessingException e) {
98 | e.printStackTrace();
99 | }
100 | JsonNode jsonNode = jsonTree.get("errorMessage");
101 |
102 | assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode());
103 | // Assert the proper error message is received
104 | assertTrue(jsonNode.asText().contains("404 Vehicle with VIN (MISSING-VIN123456) not found"));
105 | }
106 |
107 | @Test
108 | public void post_createNewVehicle_Returns_201_Created() {
109 |
110 | // Create a new vehicle
111 | Vehicle newVehicle = Vehicle.builder().vin("X0RF654S54A65E66E").make("Toyota").model("Supra").year(2020)
112 | .is_older(false).build();
113 |
114 | HttpHeaders headers = new HttpHeaders();
115 | headers.setContentType(MediaType.APPLICATION_JSON);
116 | HttpEntity request = new HttpEntity(newVehicle, headers);
117 |
118 | ResponseEntity responseEntity = this.restTemplate
119 | .postForEntity(baseUrl + port + "/demo/create/vehicle", request, Vehicle.class);
120 |
121 | // Post request should return the newly created entity back to the client
122 | assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode());
123 | assertEquals("X0RF654S54A65E66E", responseEntity.getBody().getVin());
124 | assertEquals("Toyota", responseEntity.getBody().getMake());
125 | assertEquals("Supra", responseEntity.getBody().getModel());
126 | assertFalse(responseEntity.getBody().getIs_older());
127 |
128 | // Double check this new vehicle has been stored in our embedded H2 db
129 | Optional op = vehicleRepository.findById("X0RF654S54A65E66E");
130 | assertTrue(op.isPresent());
131 | assertEquals("X0RF654S54A65E66E", op.get().getVin());
132 | }
133 |
134 | @Test
135 | public void post_createNewVehicle_Returns_400_BadRequest() {
136 |
137 | ResponseEntity result = null;
138 |
139 | // Create new vehicle with a bad VIN length for the declared model year
140 | Vehicle newVehicle = Vehicle.builder().vin("BAD-LENGTH-VIN").make("Chevrolet").model("Camaro").year(2018)
141 | .is_older(false).build();
142 |
143 | // We'll use an object mapper to show our HttpEntity also accepts JSON string
144 | ObjectMapper mapper = new ObjectMapper();
145 | JsonNode jsonNode = null;
146 | // Our post consumes JSON format
147 | HttpHeaders headers = new HttpHeaders();
148 | headers.setContentType(MediaType.APPLICATION_JSON);
149 |
150 | try {
151 | String vehicleJSONString = mapper.writeValueAsString(newVehicle);
152 |
153 | HttpEntity request = new HttpEntity(vehicleJSONString, headers);
154 | result = this.restTemplate.postForEntity(baseUrl + port + "/demo/create/vehicle", request, String.class);
155 | // Our JSON error message has an "errorMessage" attribute
156 | jsonNode = mapper.readTree(result.getBody()).get("errorMessage");
157 |
158 | } catch (JsonProcessingException e) {
159 | e.printStackTrace();
160 | } catch (Exception e) {
161 | e.printStackTrace();
162 | }
163 |
164 | assertEquals(HttpStatus.BAD_REQUEST, result.getStatusCode());
165 | // Assert the expected error message
166 | assertTrue(jsonNode.asText().contains("VIN length is invalid for the declared year"));
167 | }
168 |
169 | @Test
170 | public void put_updateVehicle_Returns_202_Accepted() {
171 |
172 | // Update vehicle. Need to update to the correct year '1992' -> '1996'
173 | Vehicle vehicleUpdate = Vehicle.builder().vin("FR4EDED2150RFT5GE").make("Ford").model("Ranger").year(1996)
174 | .is_older(false).build();
175 |
176 | // Our targeted URI consumes JSON format
177 | HttpHeaders headers = new HttpHeaders();
178 | headers.setContentType(MediaType.APPLICATION_JSON);
179 | HttpEntity requestEntity = new HttpEntity(vehicleUpdate, headers);
180 |
181 | ResponseEntity responseEntity = this.restTemplate.exchange(
182 | baseUrl + port + "/demo/update/vehicle/FR4EDED2150RFT5GE", HttpMethod.PUT, requestEntity,
183 | Vehicle.class);
184 |
185 | // Put request should return the updated vehicle entity back to the client
186 | assertEquals(HttpStatus.ACCEPTED, responseEntity.getStatusCode());
187 | assertEquals(vehicleUpdate, responseEntity.getBody());
188 | }
189 |
190 | @Test
191 | public void delete_vehicleById_Returns_NoContent_204() {
192 |
193 | ResponseEntity