Skip to content

Commit 7f95ee3

Browse files
committed
Upgrade to Spring Boot 4.0
1 parent 1bb77ad commit 7f95ee3

File tree

23 files changed

+239
-176
lines changed

23 files changed

+239
-176
lines changed

README.adoc

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,34 @@
55
:source-highlighter: prettify
66
:project_id: gs-testing-web
77

8-
This guide walks you through the process of creating a Spring application and then testing
9-
it with JUnit.
8+
This guide walks you through the process of creating a Spring application and then testing it with JUnit.
109

1110
== What You Will Build
1211

13-
You will build a simple Spring application and test it with JUnit. You probably already
14-
know how to write and run unit tests of the individual classes in your application, so,
15-
for this guide, we will concentrate on using Spring Test and Spring Boot features to test
16-
the interactions between Spring and your code. You will start with a simple test that the
17-
application context loads successfully and continue on to test only the web layer by using
18-
Spring's `MockMvc`.
12+
You will build a simple Spring application and test it with JUnit.
13+
You probably already know how to write and run unit tests of the individual classes in your application, so,
14+
for this guide, we will concentrate on using Spring Test and Spring Boot features to test the interactions between Spring and your code.
15+
You will start with a simple test that the application context loads successfully and continue on to test only the web layer by using Spring's `MockMvc`.
1916

2017

2118
== What You Need
2219

23-
:java_version: 1.8
20+
:java_version: 17
2421
include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/prereq_editor_jdk_buildtools.adoc[]
2522

2623
include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/how_to_complete_this_guide.adoc[]
2724

2825
[[scratch]]
2926
== Starting with Spring Initializr
3027

31-
You can use this https://start.spring.io/#!type=maven-project&packaging=jar&jvmVersion=17&groupId=com.example&artifactId=testing-web&name=testing-web&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.testingweb&dependencies=web[pre-initialized project] and click Generate to download a ZIP file. This project is configured to fit the examples in this tutorial.
28+
You can use this https://start.spring.io/#!type=gradle-project&packaging=jar&jvmVersion=17&groupId=com.example&artifactId=testing-web&name=testing-web&packageName=com.example.testingweb&dependencies=web,spring-restclient[pre-initialized project] and click Generate to download a ZIP file. This project is configured to fit the examples in this tutorial.
3229

3330
To manually initialize the project:
3431

3532
. Navigate to https://start.spring.io.
3633
This service pulls in all the dependencies you need for an application and does most of the setup for you.
3734
. Choose either Gradle or Maven and the language you want to use.
38-
. Click *Dependencies* and select *Spring Web*.
35+
. Click *Dependencies* and select *Spring Web* and *HTTP Client*.
3936
. Click *Generate*.
4037
. Download the resulting ZIP file, which is an archive of a web application that is configured with your choices.
4138

@@ -77,33 +74,17 @@ include::complete/src/main/java/com/example/testingweb/TestingWebApplication.jav
7774
include::complete-kotlin/src/main/kotlin/com/example/testingweb/TestingWebApplication.kt[]
7875
----
7976

80-
`@SpringBootApplication` is a convenience annotation that adds all of the following:
81-
82-
- `@Configuration`: Tags the class as a source of bean definitions for the application
83-
context.
84-
- `@EnableAutoConfiguration`: Tells Spring Boot to start adding beans based on classpath
85-
settings, other beans, and various property settings.
86-
- `@EnableWebMvc`: Flags the application as a web application and activates key behaviors,
87-
such as setting up a `DispatcherServlet`. Spring Boot adds it automatically when it sees
88-
`spring-webmvc` on the classpath.
89-
- `@ComponentScan`: Tells Spring to look for other components, configurations, and
90-
services in the package where your annotated `TestingWebApplication` class resides
91-
(`com.example.testingweb`), letting it find the `com.example.testingweb.HelloController`.
92-
93-
The `main()` method uses Spring Boot's `SpringApplication.run()` method to launch an
94-
application. Did you notice that there is not a single line of XML? There is no `web.xml`
95-
file, either. You do not have to deal with configuring any plumbing or infrastructure.
96-
Spring Boot handles all of that for you.
97-
98-
Logging output is displayed. The service should be up and running within a few seconds.
99-
100-
== Test the Application
77+
You can run this application class from your IDE directly, or from the command line using
78+
Gradle (`./gradlew :bootRun`) or Maven (`./mvwn spring-boot:run`).
10179

10280
Now that the application is running, you can test it. You can load the home page at
10381
`http://localhost:8080`. However, to give yourself more confidence that the application
10482
works when you make changes, you want to automate the testing.
10583

106-
NOTE: Spring Boot assumes you plan to test your application, so it adds the necessary
84+
85+
== Test the Application
86+
87+
Spring Boot assumes you plan to test your application, so it adds the necessary
10788
dependencies to your build file (`build.gradle` or `pom.xml`).
10889

10990
The first thing you can do is write a simple sanity check test that will fail if the
@@ -123,7 +104,7 @@ include::initial-kotlin/src/test/kotlin/com/example/testingweb/TestingWebApplica
123104
The `@SpringBootTest` annotation tells Spring Boot to look for a main configuration class
124105
(one with `@SpringBootApplication`, for instance) and use that to start a Spring
125106
application context. You can run this test in your IDE or on the command line (by running
126-
`./mvnw test` or `./gradlew test`), and it should pass. To convince yourself that the
107+
`./mvnw test` or `./gradlew check`), and it should pass. To convince yourself that the
127108
context is creating your controller, you could add an assertion, as the following example shows:
128109

129110
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
@@ -141,12 +122,6 @@ Spring interprets the `@Autowired` annotation, and the controller is injected be
141122
test methods are run. We use https://assertj.github.io/doc/[AssertJ]
142123
(which provides `assertThat()` and other methods) to express the test assertions.
143124

144-
NOTE: A nice feature of the Spring Test support is that the application context is cached
145-
between tests. That way, if you have multiple methods in a test case or multiple test
146-
cases with the same configuration, they incur the cost of starting the application only
147-
once. You can control the cache by using the {DirtiesContext}[`@DirtiesContext`]
148-
annotation.
149-
150125
It is nice to have a sanity check, but you should also write some tests that assert the
151126
behavior of your application. To do that, you could start the application and listen for a
152127
connection (as it would do in production) and then send an HTTP request and assert the
@@ -166,14 +141,16 @@ include::complete-kotlin/src/test/kotlin/com/example/testingweb/HttpRequestTest.
166141
Note the use of `webEnvironment=RANDOM_PORT` to start the server with a random port
167142
(useful to avoid conflicts in test environments) and the injection of the port with
168143
`@LocalServerPort`. Also, note that Spring Boot has automatically provided a
169-
`TestRestTemplate` for you. All you have to do is add `@Autowired` to it.
144+
`RestTestClient` for you because we expressed the need for it with `@AutoConfigureRestTestClient`.
145+
All you have to do after that is to add `@Autowired` to the field.
170146

171147
Another useful approach is to not start the server at all but to test only the layer below
172148
that, where Spring handles the incoming HTTP request and hands it off to your controller.
173149
That way, almost all of the full stack is used, and your code will be called in exactly the
174150
same way as if it were processing a real HTTP request but without the cost of starting the
175-
server. To do that, use Spring's `MockMvc` and ask for that to be injected for you by
176-
using the `@AutoConfigureMockMvc` annotation on the test case. The following listing shows how to do so:
151+
server. To do that, we can reuse our previous test using `RestTestClient`, but this time
152+
leaving `@SpringBootTest` with its default, which is to start a mock server environment.
153+
The following listing shows how to do so:
177154

178155
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
179156
.Java

complete-kotlin/build.gradle.kts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,39 @@
11
plugins {
2-
id("org.springframework.boot") version "3.5.7"
2+
kotlin("jvm") version "2.2.21"
3+
kotlin("plugin.spring") version "2.2.21"
4+
id("org.springframework.boot") version "4.0.0"
35
id("io.spring.dependency-management") version "1.1.7"
4-
kotlin("jvm") version "1.9.25"
5-
kotlin("plugin.spring") version "1.9.25"
66
}
77

88
group = "com.example"
99
version = "0.0.1-SNAPSHOT"
10-
java.sourceCompatibility = JavaVersion.VERSION_17
10+
description = "Demo project for Spring Boot"
11+
12+
java {
13+
toolchain {
14+
languageVersion = JavaLanguageVersion.of(17)
15+
}
16+
}
1117

1218
repositories {
1319
mavenCentral()
1420
}
1521

1622
dependencies {
17-
implementation("org.springframework.boot:spring-boot-starter-web")
18-
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
23+
implementation("org.springframework.boot:spring-boot-starter-restclient")
24+
implementation("org.springframework.boot:spring-boot-starter-webmvc")
1925
implementation("org.jetbrains.kotlin:kotlin-reflect")
20-
testImplementation("org.springframework.boot:spring-boot-starter-test")
26+
implementation("tools.jackson.module:jackson-module-kotlin")
27+
testImplementation("org.springframework.boot:spring-boot-starter-restclient-test")
28+
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
29+
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
30+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
2131
}
2232

2333
kotlin {
24-
jvmToolchain(17)
34+
compilerOptions {
35+
freeCompilerArgs.addAll("-Xjsr305=strict", "-Xannotation-default-target=param-property")
36+
}
2537
}
2638

2739
tasks.withType<Test> {

complete-kotlin/gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
44
networkTimeout=10000
55
validateDistributionUrl=true
66
zipStoreBase=GRADLE_USER_HOME
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
spring.application.name=testing-web
Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
11
package com.example.testingweb
22

3-
import org.assertj.core.api.Assertions.assertThat
43
import org.junit.jupiter.api.Test
54
import org.springframework.beans.factory.annotation.Autowired
5+
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
66
import org.springframework.boot.test.context.SpringBootTest
7-
import org.springframework.boot.test.web.client.TestRestTemplate
87
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
9-
import org.springframework.boot.test.web.client.getForObject
108
import org.springframework.boot.test.web.server.LocalServerPort
9+
import org.springframework.test.web.servlet.client.RestTestClient
10+
import org.springframework.test.web.servlet.client.expectBody
1111

1212
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
13+
@AutoConfigureRestTestClient
1314
class HttpRequestTest {
1415

1516
@LocalServerPort
1617
private var port: Int = 0
1718

1819
@Autowired
19-
private lateinit var restTemplate: TestRestTemplate
20+
private lateinit var restTestClient: RestTestClient
2021

2122
@Test
2223
fun greetingShouldReturnDefaultMessage() {
23-
// Import Kotlin .getForObject() extension that allows using reified type parameters
24-
assertThat(this.restTemplate.getForObject<String>("http://localhost:$port/"))
25-
.contains("Hello, World")
24+
// Import Kotlin .expectBody() extension that allows using reified type parameters
25+
restTestClient.get()
26+
.uri("http://localhost:$port/")
27+
.exchange()
28+
.expectBody<String>()
29+
.isEqualTo("Hello, World")
2630
}
27-
}
31+
32+
}
Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
11
package com.example.testingweb
22

3-
import org.hamcrest.Matchers.containsString
43
import org.junit.jupiter.api.Test
54
import org.springframework.beans.factory.annotation.Autowired
6-
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
5+
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
76
import org.springframework.boot.test.context.SpringBootTest
8-
import org.springframework.test.web.servlet.MockMvc
9-
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
10-
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
11-
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
12-
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
7+
import org.springframework.test.web.servlet.client.RestTestClient
8+
import org.springframework.test.web.servlet.client.expectBody
139

1410
@SpringBootTest
15-
@AutoConfigureMockMvc
11+
@AutoConfigureRestTestClient
1612
class TestingWebApplicationTest {
1713

1814
@Autowired
19-
private lateinit var mockMvc: MockMvc
15+
private lateinit var restTestClient: RestTestClient
2016

2117
@Test
22-
fun shouldReturnDefaultMessage() {
23-
mockMvc.perform(get("/"))
24-
.andDo(print())
25-
.andExpect(status().isOk())
26-
.andExpect(content().string(containsString("Hello, World")))
18+
fun greetingShouldReturnDefaultMessage() {
19+
// Import Kotlin .expectBody() extension that allows using reified type parameters
20+
restTestClient.get().uri("/")
21+
.exchange()
22+
.expectBody<String>()
23+
.isEqualTo("Hello, World")
2724
}
25+
2826
}
Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
package com.example.testingweb
22

3-
import org.hamcrest.Matchers.containsString
43
import org.junit.jupiter.api.Test
54
import org.springframework.beans.factory.annotation.Autowired
6-
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
7-
import org.springframework.test.web.servlet.MockMvc
8-
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
9-
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
10-
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
11-
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
5+
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
6+
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
7+
import org.springframework.test.web.servlet.client.RestTestClient
8+
import org.springframework.test.web.servlet.client.expectBody
129

1310
@WebMvcTest(HomeController::class)
11+
@AutoConfigureRestTestClient
1412
class WebLayerTest {
1513

1614
@Autowired
17-
private lateinit var mockMvc: MockMvc
15+
private lateinit var restTestClient: RestTestClient
1816

1917
@Test
20-
fun shouldReturnDefaultMessage() {
21-
mockMvc.perform(get("/"))
22-
.andDo(print())
23-
.andExpect(status().isOk())
24-
.andExpect(content().string(containsString("Hello, World")))
18+
fun greetingShouldReturnDefaultMessage() {
19+
// Import Kotlin .expectBody() extension that allows using reified type parameters
20+
restTestClient.get()
21+
.uri("/")
22+
.exchange()
23+
.expectBody<String>()
24+
.isEqualTo("Hello, World")
2525
}
26+
2627
}
Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
package com.example.testingweb
22

3-
import org.hamcrest.Matchers.containsString
43
import org.junit.jupiter.api.Test
54
import org.mockito.Mockito.`when`
65
import org.springframework.beans.factory.annotation.Autowired
7-
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
6+
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
7+
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
88
import org.springframework.test.context.bean.override.mockito.MockitoBean
9-
import org.springframework.test.web.servlet.MockMvc
10-
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
11-
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
12-
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
13-
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
9+
import org.springframework.test.web.servlet.client.RestTestClient
10+
import org.springframework.test.web.servlet.client.expectBody
1411

1512
@WebMvcTest(GreetingController::class)
13+
@AutoConfigureRestTestClient
1614
class WebMockTest {
1715

1816
@Autowired
19-
private lateinit var mockMvc: MockMvc
17+
private lateinit var restTestClient: RestTestClient
2018

2119
@MockitoBean
2220
private lateinit var service: GreetingService
2321

2422
@Test
2523
fun greetingShouldReturnMessageFromService() {
2624
`when`(service.greet()).thenReturn("Hello, Mock")
27-
mockMvc.perform(get("/greeting"))
28-
.andDo(print())
29-
.andExpect(status().isOk())
30-
.andExpect(content().string(containsString("Hello, Mock")))
25+
// Import Kotlin .expectBody() extension that allows using reified type parameters
26+
restTestClient.get()
27+
.uri("/greeting")
28+
.exchange()
29+
.expectBody<String>()
30+
.isEqualTo("Hello, Mock")
3131
}
3232
}

complete/build.gradle

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
plugins {
2-
id 'org.springframework.boot' version '3.5.7'
3-
id 'io.spring.dependency-management' version '1.1.7'
42
id 'java'
3+
id 'org.springframework.boot' version '4.0.0'
4+
id 'io.spring.dependency-management' version '1.1.7'
55
}
66

77
group = 'com.example'
88
version = '0.0.1-SNAPSHOT'
9-
sourceCompatibility = '17'
9+
description = 'Demo project for Spring Boot'
10+
11+
java {
12+
toolchain {
13+
languageVersion = JavaLanguageVersion.of(17)
14+
}
15+
}
1016

1117
repositories {
1218
mavenCentral()
1319
}
1420

1521
dependencies {
16-
implementation 'org.springframework.boot:spring-boot-starter-web'
17-
testImplementation('org.springframework.boot:spring-boot-starter-test')
22+
implementation 'org.springframework.boot:spring-boot-starter-restclient'
23+
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
24+
testImplementation 'org.springframework.boot:spring-boot-starter-restclient-test'
25+
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
1826
}
1927

20-
test {
28+
tasks.named('test') {
2129
useJUnitPlatform()
2230
}

0 commit comments

Comments
 (0)