Skip to content

Commit 5812b69

Browse files
refac: Update Spring Boot from 3.5.8 to 4.0.0 (#93)
- Update Spring Boot from 3.5.8 to 4.0.0. - Update `junit-platform-launcher` from 1.12.2 to 6.0.1. - Update Springdoc from 2.8.14 to 3.0.0. - Remove dependency `org.liquibase:liquibase-core`. - Add dependency `spring-boot-starter-liquibase`. - Add dependency `spring-boot-resttestclient`. - Add dependency `org.testcontainers:junit-jupiter`. - Migrate dependency from `spring-boot-starter-web` to `spring-boot-starter-webmvc`. - Remove `TestContainerConfig` and instead set up a `PostgreSQLContainer` in the necessary tests with PostgreSQL 18. - Use `JsonMapper` instead of `ObjectMapper` in tests as this is the new recommended way to serialize JSON using Jackson 3+. - Rename `BaseMockMvc` to `BaseControllerTest` and start using the new `RestTestClient` from Spring Boot 4+.
1 parent 7e039e4 commit 5812b69

File tree

7 files changed

+108
-128
lines changed

7 files changed

+108
-128
lines changed

apps/api/build.gradle.kts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,28 @@ dependencies {
1212
// Spring Boot dependencies
1313
implementation(local.springboot.starter)
1414
implementation(local.springboot.starter.validation)
15-
implementation(local.springboot.starter.web)
15+
implementation(local.springboot.starter.webmvc)
16+
17+
// Spring Boot Liquibase dependency for database migrations
18+
implementation(local.springboot.starter.liquibase)
1619

1720
// H2 database dependency for in-memory database
1821
runtimeOnly(local.h2database)
1922

2023
// FasterXML Jackson support for Java 8 date/time
2124
implementation(local.jackson.datatype.jsr310)
2225

23-
// Liquibase core dependency for database migrations
24-
runtimeOnly(local.liquibase.core)
25-
2626
// PostgreSQL database driver
2727
runtimeOnly(local.postgres)
2828

2929
// Springdoc OpenAPI for providing Swagger documentation
3030
implementation(local.springdoc.openapi.starter.webmvc)
3131

3232
// Spring Boot and Testcontainers test dependencies
33+
testImplementation(local.springboot.resttestclient)
3334
testImplementation(local.springboot.starter.test)
3435
testImplementation(local.springboot.testcontainers)
36+
testImplementation(local.testcontainers.junit.jupiter)
3537
testImplementation(local.testcontainers.postgresql)
3638

3739
// JUnit platform launcher dependency for running JUnit tests
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.github.thorlauridsen;
2+
3+
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
5+
import org.springframework.boot.test.context.SpringBootTest;
6+
import org.springframework.http.HttpHeaders;
7+
import org.springframework.http.MediaType;
8+
import org.springframework.test.web.servlet.client.RestTestClient;
9+
10+
/**
11+
* This is the BaseMockMvc class that allows you to send and test HTTP requests.
12+
*/
13+
@AutoConfigureRestTestClient
14+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
15+
public class BaseControllerTest {
16+
17+
@Autowired
18+
private RestTestClient restTestClient;
19+
20+
/**
21+
* Test an HTTP GET request.
22+
*
23+
* @param getUrl the URL to send an HTTP GET request to.
24+
* @return {@link RestTestClient.ResponseSpec} response.
25+
*/
26+
public RestTestClient.ResponseSpec get(String getUrl) {
27+
return restTestClient.get()
28+
.uri(getUrl)
29+
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
30+
.exchange();
31+
}
32+
33+
/**
34+
* Test an HTTP POST request.
35+
*
36+
* @param postUrl the URL to send an HTTP POST request to.
37+
* @param jsonBody the JSON body to send with the request.
38+
* @return {@link RestTestClient.ResponseSpec} response.
39+
*/
40+
public RestTestClient.ResponseSpec post(String postUrl, String jsonBody) {
41+
return restTestClient.post()
42+
.uri(postUrl)
43+
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
44+
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
45+
.body(jsonBody)
46+
.exchange();
47+
}
48+
}

apps/api/src/test/java/com/github/thorlauridsen/BaseMockMvc.java

Lines changed: 0 additions & 54 deletions
This file was deleted.

apps/api/src/test/java/com/github/thorlauridsen/CustomerControllerTest.java

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.github.thorlauridsen;
22

3-
import com.fasterxml.jackson.databind.ObjectMapper;
43
import com.github.thorlauridsen.dto.CustomerDto;
54
import com.github.thorlauridsen.dto.CustomerInputDto;
65
import com.github.thorlauridsen.dto.ErrorDto;
@@ -10,10 +9,13 @@
109
import org.junit.jupiter.params.ParameterizedTest;
1110
import org.junit.jupiter.params.provider.ValueSource;
1211
import org.springframework.beans.factory.annotation.Autowired;
13-
import org.springframework.context.annotation.Import;
12+
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
1413
import org.springframework.http.HttpStatus;
1514
import org.springframework.test.context.ActiveProfiles;
16-
import org.springframework.test.web.servlet.MockMvc;
15+
import org.testcontainers.containers.PostgreSQLContainer;
16+
import org.testcontainers.junit.jupiter.Container;
17+
import org.testcontainers.junit.jupiter.Testcontainers;
18+
import tools.jackson.databind.json.JsonMapper;
1719

1820
import static com.github.thorlauridsen.controller.BaseEndpoint.CUSTOMER_BASE_ENDPOINT;
1921
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -26,74 +28,71 @@
2628
* A local Docker instance is required to run the tests as Testcontainers is used.
2729
*/
2830
@ActiveProfiles("postgres")
29-
@Import(TestContainerConfig.class)
30-
class CustomerControllerTest extends BaseMockMvc {
31+
@Testcontainers
32+
class CustomerControllerTest extends BaseControllerTest {
3133

32-
@Autowired
33-
private ObjectMapper objectMapper;
34+
@Container
35+
@ServiceConnection
36+
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:18");
3437

3538
@Autowired
36-
public CustomerControllerTest(MockMvc mockMvc) {
37-
super(mockMvc);
38-
}
39+
private JsonMapper jsonMapper;
3940

4041
@Test
41-
void getCustomer_randomId_returnsNotFound() throws Exception {
42+
void getCustomer_randomId_returnsNotFound() {
4243
val id = UUID.randomUUID();
43-
val response = mockGet(CUSTOMER_BASE_ENDPOINT + "/" + id);
44-
assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatus());
44+
val response = get(CUSTOMER_BASE_ENDPOINT + "/" + id);
45+
46+
response.expectStatus().isEqualTo(HttpStatus.NOT_FOUND);
4547
}
4648

4749
@ParameterizedTest
4850
@ValueSource(strings = {
4951
"alice@gmail.com",
5052
"bob@gmail.com"
5153
})
52-
void postCustomer_getCustomer_success(String mail) throws Exception {
54+
void postCustomer_getCustomer_success(String mail) {
5355
val customer = new CustomerInputDto(mail);
54-
val json = objectMapper.writeValueAsString(customer);
55-
val response = mockPost(json, CUSTOMER_BASE_ENDPOINT);
56-
assertEquals(HttpStatus.CREATED.value(), response.getStatus());
56+
val json = jsonMapper.writeValueAsString(customer);
57+
val response = post(CUSTOMER_BASE_ENDPOINT, json);
58+
response.expectStatus().isEqualTo(HttpStatus.CREATED);
5759

58-
val responseJson = response.getContentAsString();
59-
val createdCustomer = objectMapper.readValue(responseJson, CustomerDto.class);
60+
val createdCustomer = response.expectBody(CustomerDto.class).returnResult().getResponseBody();
61+
assertNotNull(createdCustomer);
6062
assertCustomer(createdCustomer, mail);
6163

62-
val response2 = mockGet(CUSTOMER_BASE_ENDPOINT + "/" + createdCustomer.id());
63-
assertEquals(HttpStatus.OK.value(), response2.getStatus());
64+
val response2 = get(CUSTOMER_BASE_ENDPOINT + "/" + createdCustomer.id());
65+
response2.expectStatus().isEqualTo(HttpStatus.OK);
6466

65-
val responseJson2 = response2.getContentAsString();
66-
val fetchedCustomer = objectMapper.readValue(responseJson2, CustomerDto.class);
67+
val fetchedCustomer = response2.expectBody(CustomerDto.class).returnResult().getResponseBody();
6768
assertCustomer(fetchedCustomer, mail);
6869
}
6970

7071
@Test
71-
void postCustomer_blankEmail_returnsBadRequest() throws Exception {
72+
void postCustomer_blankEmail_returnsBadRequest() {
7273
val customer = new CustomerInputDto("");
73-
val json = objectMapper.writeValueAsString(customer);
74-
val response = mockPost(json, CUSTOMER_BASE_ENDPOINT);
74+
val json = jsonMapper.writeValueAsString(customer);
75+
val response = post(CUSTOMER_BASE_ENDPOINT, json);
7576

76-
assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatus());
77-
78-
val responseJson = response.getContentAsString();
79-
val error = objectMapper.readValue(responseJson, ErrorDto.class);
77+
response.expectStatus().isEqualTo(HttpStatus.BAD_REQUEST);
78+
val error = response.expectBody(ErrorDto.class).returnResult().getResponseBody();
8079

80+
assertNotNull(error);
8181
assertEquals("Validation failed", error.description());
8282
assertTrue(error.fieldErrors().containsKey("mail"));
8383
assertEquals("Email is required", error.fieldErrors().get("mail"));
8484
}
8585

8686
@Test
87-
void postCustomer_invalidEmailFormat_returnsBadRequest() throws Exception {
87+
void postCustomer_invalidEmailFormat_returnsBadRequest() {
8888
val customer = new CustomerInputDto("invalid-email");
89-
val json = objectMapper.writeValueAsString(customer);
90-
val response = mockPost(json, CUSTOMER_BASE_ENDPOINT);
91-
92-
assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatus());
89+
val json = jsonMapper.writeValueAsString(customer);
90+
val response = post(CUSTOMER_BASE_ENDPOINT, json);
9391

94-
val responseJson = response.getContentAsString();
95-
val error = objectMapper.readValue(responseJson, ErrorDto.class);
92+
response.expectStatus().isEqualTo(HttpStatus.BAD_REQUEST);
93+
val error = response.expectBody(ErrorDto.class).returnResult().getResponseBody();
9694

95+
assertNotNull(error);
9796
assertEquals("Validation failed", error.description());
9897
assertTrue(error.fieldErrors().containsKey("mail"));
9998
assertEquals("Invalid email format", error.fieldErrors().get("mail"));

apps/api/src/test/java/com/github/thorlauridsen/CustomerServiceTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
import org.junit.jupiter.params.provider.ValueSource;
1313
import org.springframework.beans.factory.annotation.Autowired;
1414
import org.springframework.boot.test.context.SpringBootTest;
15+
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
16+
import org.springframework.test.context.ActiveProfiles;
17+
import org.testcontainers.containers.PostgreSQLContainer;
18+
import org.testcontainers.junit.jupiter.Container;
19+
import org.testcontainers.junit.jupiter.Testcontainers;
1520

1621
import static org.junit.jupiter.api.Assertions.assertEquals;
1722
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -22,9 +27,15 @@
2227
* This class uses the @SpringBootTest annotation to spin up a Spring Boot instance.
2328
* This ensures that Spring can automatically inject {@link CustomerService} with a {@link CustomerRepo}
2429
*/
30+
@ActiveProfiles("postgres")
2531
@SpringBootTest
32+
@Testcontainers
2633
class CustomerServiceTest {
2734

35+
@Container
36+
@ServiceConnection
37+
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:18");
38+
2839
@Autowired
2940
private CustomerService customerService;
3041

apps/api/src/test/java/com/github/thorlauridsen/TestContainerConfig.java

Lines changed: 0 additions & 25 deletions
This file was deleted.

gradle/local.versions.toml

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
[versions]
22
h2database = "2.4.240"
33
jackson = "2.20.1"
4-
junitPlatformLauncher = "1.12.2"
5-
liquibase = "5.0.1"
4+
junitPlatformLauncher = "6.0.1"
65
lombok = "9.1.0"
76
postgres = "42.7.8"
8-
springboot = "3.5.8"
7+
springboot = "4.0.0"
98
springDependencyPlugin = "1.1.7"
10-
springdoc = "2.8.14"
9+
springdoc = "3.0.0"
1110
testcontainers = "1.21.3"
1211

1312
[libraries]
@@ -20,21 +19,21 @@ jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-dat
2019
# JUnit platform launcher for running JUnit tests
2120
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junitPlatformLauncher" }
2221

23-
# Liquibase for managing database changelogs
24-
liquibase-core = { module = "org.liquibase:liquibase-core", version.ref = "liquibase" }
25-
2622
# PostgreSQL for a live database
2723
postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" }
2824

2925
# Spring Boot libraries
26+
springboot-resttestclient = { module = 'org.springframework.boot:spring-boot-resttestclient', version.ref = "springboot" }
3027
springboot-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springboot" }
3128
springboot-starter-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa", version.ref = "springboot" }
29+
springboot-starter-liquibase = { module = "org.springframework.boot:spring-boot-starter-liquibase", version.ref = "springboot" }
3230
springboot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "springboot" }
3331
springboot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation", version.ref = "springboot" }
34-
springboot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "springboot" }
32+
springboot-starter-webmvc = { module = "org.springframework.boot:spring-boot-starter-webmvc", version.ref = "springboot" }
3533
springboot-testcontainers = { module = "org.springframework.boot:spring-boot-testcontainers", version.ref = "springboot" }
3634

3735
# Testcontainers for running PostgreSQL in tests
36+
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" }
3837
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" }
3938

4039
# Springdoc provides swagger docs with support for Spring Web MVC

0 commit comments

Comments
 (0)