Skip to content

Commit fb8a1be

Browse files
authored
Merge pull request #18736 from eugenp/add-missing-code
add missing code for httpinterface article
2 parents 0455d94 + 8945030 commit fb8a1be

File tree

6 files changed

+303
-25
lines changed

6 files changed

+303
-25
lines changed

spring-boot-modules/spring-boot-3-2/pom.xml

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,9 @@
1515
</parent>
1616

1717
<dependencies>
18-
<!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
19-
<dependency>
20-
<groupId>org.springframework</groupId>
21-
<artifactId>spring-web</artifactId>
22-
<version>6.2.3</version>
23-
</dependency>
24-
<dependency>
25-
<groupId>org.springframework</groupId>
26-
<artifactId>spring-core</artifactId>
27-
<version>6.2.3</version>
28-
</dependency>
2918
<dependency>
3019
<groupId>org.springframework.boot</groupId>
3120
<artifactId>spring-boot-starter-web</artifactId>
32-
<version>3.4.3</version>
3321
</dependency>
3422
<dependency>
3523
<groupId>org.springframework.boot</groupId>
@@ -81,7 +69,6 @@
8169
<dependency>
8270
<groupId>org.projectlombok</groupId>
8371
<artifactId>lombok</artifactId>
84-
<version>${lombok.version}</version>
8572
<optional>true</optional>
8673
</dependency>
8774
<dependency>
@@ -105,7 +92,6 @@
10592
<dependency>
10693
<groupId>org.springframework.boot</groupId>
10794
<artifactId>spring-boot-starter-test</artifactId>
108-
<version>3.4.3</version>
10995
</dependency>
11096
<dependency>
11197
<groupId>org.postgresql</groupId>
@@ -121,14 +107,9 @@
121107
<artifactId>spring-rabbit-test</artifactId>
122108
<scope>test</scope>
123109
</dependency>
124-
<dependency>
125-
<groupId>org.springframework.data</groupId>
126-
<artifactId>spring-data-redis</artifactId>
127-
</dependency>
128110
<dependency>
129111
<groupId>redis.clients</groupId>
130112
<artifactId>jedis</artifactId>
131-
<version>${jedis.version}</version>
132113
<type>jar</type>
133114
</dependency>
134115
<dependency>
@@ -272,8 +253,8 @@
272253

273254
<properties>
274255
<start-class>com.baeldung.restclient.RestClientApplication</start-class>
275-
<mapstruct.version>1.6.0.Beta1</mapstruct.version>
276-
<mockserver.version>5.14.0</mockserver.version>
256+
<mapstruct.version>1.6.0</mapstruct.version>
257+
<mockserver.version>5.15.0</mockserver.version>
277258
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
278259
<jedis.version>5.0.2</jedis.version>
279260
<spring-kafka.version>3.1.2</spring-kafka.version>

spring-boot-modules/spring-boot-3-2/src/main/java/com/baeldung/dockercompose/controller/ItemController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
@RequiredArgsConstructor
2323
public class ItemController {
2424

25-
private final ItemRepository itemRepository;
26-
25+
private ItemRepository itemRepository;
26+
2727
@PostMapping(consumes = APPLICATION_JSON_VALUE)
2828
public ResponseEntity<Item> save(final @RequestBody Item item) {
2929
return ResponseEntity.ok(itemRepository.save(item));

spring-boot-modules/spring-boot-3-2/src/main/java/com/baeldung/httpinterface/BooksClient.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.springframework.stereotype.Component;
44
import org.springframework.web.reactive.function.client.WebClient;
5+
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
56
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
67

78
@Component
@@ -10,8 +11,7 @@ public class BooksClient {
1011
private final BooksService booksService;
1112

1213
public BooksClient(WebClient webClient) {
13-
HttpServiceProxyFactory httpServiceProxyFactory =
14-
HttpServiceProxyFactory.builder()
14+
HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient))
1515
.build();
1616
booksService = httpServiceProxyFactory.createClient(BooksService.class);
1717
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package com.baeldung.httpinterface;
2+
3+
import org.apache.http.HttpStatus;
4+
import org.junit.jupiter.api.AfterAll;
5+
import org.junit.jupiter.api.BeforeAll;
6+
import org.junit.jupiter.api.Test;
7+
import org.mockserver.client.MockServerClient;
8+
import org.mockserver.integration.ClientAndServer;
9+
import org.mockserver.configuration.Configuration;
10+
11+
import java.io.IOException;
12+
import java.net.ServerSocket;
13+
import java.util.List;
14+
15+
import org.mockserver.model.HttpRequest;
16+
import org.mockserver.model.MediaType;
17+
import org.mockserver.verify.VerificationTimes;
18+
import org.slf4j.event.Level;
19+
import org.springframework.http.HttpMethod;
20+
import org.springframework.http.HttpStatusCode;
21+
import org.springframework.http.ResponseEntity;
22+
import org.springframework.web.reactive.function.client.WebClient;
23+
import org.springframework.web.reactive.function.client.WebClientResponseException;
24+
import reactor.core.publisher.Mono;
25+
26+
import static org.junit.jupiter.api.Assertions.assertThrows;
27+
import static org.mockserver.integration.ClientAndServer.startClientAndServer;
28+
import static org.mockserver.matchers.Times.exactly;
29+
import static org.mockserver.model.HttpRequest.request;
30+
import static org.mockserver.model.HttpResponse.response;
31+
import static org.junit.jupiter.api.Assertions.assertEquals;
32+
33+
class BooksServiceMockServerTest {
34+
35+
private static final String SERVER_ADDRESS = "localhost";
36+
private static final String PATH = "/books";
37+
38+
private static int serverPort;
39+
private static ClientAndServer mockServer;
40+
private static String serviceUrl;
41+
42+
@BeforeAll
43+
static void startServer() throws IOException {
44+
serverPort = getFreePort();
45+
serviceUrl = "http://" + SERVER_ADDRESS + ":" + serverPort;
46+
47+
Configuration config = Configuration.configuration().logLevel(Level.WARN);
48+
mockServer = startClientAndServer(config, serverPort);
49+
50+
mockAllBooksRequest();
51+
mockBookByIdRequest();
52+
mockSaveBookRequest();
53+
mockDeleteBookRequest();
54+
}
55+
56+
@AfterAll
57+
static void stopServer() {
58+
mockServer.stop();
59+
}
60+
61+
@Test
62+
void givenMockedGetResponse_whenGetBooksServiceMethodIsCalled_thenTwoBooksAreReturned() {
63+
BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
64+
BooksService booksService = booksClient.getBooksService();
65+
66+
List<Book> books = booksService.getBooks();
67+
assertEquals(2, books.size());
68+
69+
mockServer.verify(
70+
HttpRequest.request()
71+
.withMethod(HttpMethod.GET.name())
72+
.withPath(PATH),
73+
VerificationTimes.exactly(1)
74+
);
75+
}
76+
77+
@Test
78+
void givenMockedGetResponse_whenGetExistingBookServiceMethodIsCalled_thenCorrectBookIsReturned() {
79+
BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
80+
BooksService booksService = booksClient.getBooksService();
81+
82+
Book book = booksService.getBook(1);
83+
assertEquals("Book_1", book.title());
84+
85+
mockServer.verify(
86+
HttpRequest.request()
87+
.withMethod(HttpMethod.GET.name())
88+
.withPath(PATH + "/1"),
89+
VerificationTimes.exactly(1)
90+
);
91+
}
92+
93+
@Test
94+
void givenMockedGetResponse_whenGetNonExistingBookServiceMethodIsCalled_thenCorrectBookIsReturned() {
95+
BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
96+
BooksService booksService = booksClient.getBooksService();
97+
98+
assertThrows(WebClientResponseException.class, () -> booksService.getBook(9));
99+
}
100+
101+
@Test
102+
void givenCustomErrorHandlerIsSet_whenGetNonExistingBookServiceMethodIsCalled_thenCustomExceptionIsThrown() {
103+
BooksClient booksClient = new BooksClient(WebClient.builder()
104+
.defaultStatusHandler(HttpStatusCode::isError, resp ->
105+
Mono.just(new MyServiceException("Custom exception")))
106+
.baseUrl(serviceUrl)
107+
.build());
108+
109+
BooksService booksService = booksClient.getBooksService();
110+
assertThrows(MyServiceException.class, () -> booksService.getBook(9));
111+
}
112+
113+
@Test
114+
void givenMockedPostResponse_whenSaveBookServiceMethodIsCalled_thenCorrectBookIsReturned() {
115+
BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
116+
BooksService booksService = booksClient.getBooksService();
117+
118+
Book book = booksService.saveBook(new Book(3, "Book_3", "Author_3", 2000));
119+
assertEquals("Book_3", book.title());
120+
121+
mockServer.verify(
122+
HttpRequest.request()
123+
.withMethod(HttpMethod.POST.name())
124+
.withPath(PATH),
125+
VerificationTimes.exactly(1)
126+
);
127+
}
128+
129+
@Test
130+
void givenMockedDeleteResponse_whenDeleteBookServiceMethodIsCalled_thenCorrectCodeIsReturned() {
131+
BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
132+
BooksService booksService = booksClient.getBooksService();
133+
134+
ResponseEntity<Void> response = booksService.deleteBook(3);
135+
assertEquals(HttpStatusCode.valueOf(200), response.getStatusCode());
136+
137+
mockServer.verify(
138+
HttpRequest.request()
139+
.withMethod(HttpMethod.DELETE.name())
140+
.withPath(PATH + "/3"),
141+
VerificationTimes.exactly(1)
142+
);
143+
}
144+
145+
private static int getFreePort () throws IOException {
146+
try (ServerSocket serverSocket = new ServerSocket(0)) {
147+
return serverSocket.getLocalPort();
148+
}
149+
}
150+
151+
private static void mockAllBooksRequest() {
152+
new MockServerClient(SERVER_ADDRESS, serverPort)
153+
.when(
154+
request()
155+
.withPath(PATH)
156+
.withMethod(HttpMethod.GET.name()),
157+
exactly(1)
158+
)
159+
.respond(
160+
response()
161+
.withStatusCode(HttpStatus.SC_OK)
162+
.withContentType(MediaType.APPLICATION_JSON)
163+
.withBody("[{\"id\":1,\"title\":\"Book_1\",\"author\":\"Author_1\",\"year\":1998},{\"id\":2,\"title\":\"Book_2\",\"author\":\"Author_2\",\"year\":1999}]")
164+
);
165+
}
166+
167+
private static void mockBookByIdRequest() {
168+
new MockServerClient(SERVER_ADDRESS, serverPort)
169+
.when(
170+
request()
171+
.withPath(PATH + "/1")
172+
.withMethod(HttpMethod.GET.name()),
173+
exactly(1)
174+
)
175+
.respond(
176+
response()
177+
.withStatusCode(HttpStatus.SC_OK)
178+
.withContentType(MediaType.APPLICATION_JSON)
179+
.withBody("{\"id\":1,\"title\":\"Book_1\",\"author\":\"Author_1\",\"year\":1998}")
180+
);
181+
}
182+
183+
private static void mockSaveBookRequest() {
184+
new MockServerClient(SERVER_ADDRESS, serverPort)
185+
.when(
186+
request()
187+
.withPath(PATH)
188+
.withMethod(HttpMethod.POST.name())
189+
.withContentType(MediaType.APPLICATION_JSON)
190+
.withBody("{\"id\":3,\"title\":\"Book_3\",\"author\":\"Author_3\",\"year\":2000}"),
191+
exactly(1)
192+
)
193+
.respond(
194+
response()
195+
.withStatusCode(HttpStatus.SC_OK)
196+
.withContentType(MediaType.APPLICATION_JSON)
197+
.withBody("{\"id\":3,\"title\":\"Book_3\",\"author\":\"Author_3\",\"year\":2000}")
198+
);
199+
}
200+
201+
private static void mockDeleteBookRequest() {
202+
new MockServerClient(SERVER_ADDRESS, serverPort)
203+
.when(
204+
request()
205+
.withPath(PATH + "/3")
206+
.withMethod(HttpMethod.DELETE.name()),
207+
exactly(1)
208+
)
209+
.respond(
210+
response()
211+
.withStatusCode(HttpStatus.SC_OK)
212+
);
213+
}
214+
215+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.baeldung.httpinterface;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.mockito.ArgumentMatchers.anyMap;
5+
import static org.mockito.ArgumentMatchers.anyString;
6+
import static org.mockito.BDDMockito.given;
7+
8+
import java.util.List;
9+
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.ExtendWith;
12+
import org.mockito.Answers;
13+
import org.mockito.InjectMocks;
14+
import org.mockito.Mock;
15+
import org.mockito.junit.jupiter.MockitoExtension;
16+
import org.springframework.core.ParameterizedTypeReference;
17+
import org.springframework.http.HttpMethod;
18+
import org.springframework.web.reactive.function.client.WebClient;
19+
20+
import reactor.core.publisher.Mono;
21+
22+
@ExtendWith(MockitoExtension.class)
23+
class BooksServiceMockitoUnitTest {
24+
25+
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
26+
private WebClient webClient;
27+
28+
@InjectMocks
29+
private BooksClient booksClient;
30+
31+
@Test
32+
void givenMockedWebClientReturnsTwoBooks_whenGetBooksServiceMethodIsCalled_thenListOfTwoBooksIsReturned() {
33+
given(webClient.method(HttpMethod.GET)
34+
.uri(anyString(), anyMap())
35+
.retrieve()
36+
.bodyToMono(new ParameterizedTypeReference<List<Book>>(){}))
37+
.willReturn(Mono.just(List.of(
38+
new Book(1,"Book_1", "Author_1", 1998),
39+
new Book(2, "Book_2", "Author_2", 1999)
40+
)));
41+
42+
BooksService booksService = booksClient.getBooksService();
43+
List<Book> books = booksService.getBooks();
44+
assertEquals(2, books.size());
45+
}
46+
47+
@Test
48+
void givenMockedWebClientReturnsBook_whenGetBookServiceMethodIsCalled_thenBookIsReturned() {
49+
given(webClient.method(HttpMethod.GET)
50+
.uri(anyString(), anyMap())
51+
.retrieve()
52+
.bodyToMono(new ParameterizedTypeReference<Book>(){}))
53+
.willReturn(Mono.just(new Book(1,"Book_1", "Author_1", 1998)));
54+
55+
BooksService booksService = booksClient.getBooksService();
56+
Book book = booksService.getBook(1);
57+
assertEquals("Book_1", book.title());
58+
}
59+
60+
@Test
61+
void givenMockedWebClientReturnsBook_whenSaveBookServiceMethodIsCalled_thenBookIsReturned() {
62+
given(webClient.method(HttpMethod.POST)
63+
.uri(anyString(), anyMap())
64+
.retrieve()
65+
.bodyToMono(new ParameterizedTypeReference<Book>(){}))
66+
.willReturn(Mono.just(new Book(3, "Book_3", "Author_3", 2000)));
67+
68+
BooksService booksService = booksClient.getBooksService();
69+
Book book = booksService.saveBook(new Book(3, "Book_3", "Author_3", 2000));
70+
assertEquals("Book_3", book.title());
71+
}
72+
73+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.baeldung.httpinterface;
2+
3+
public class MyServiceException extends RuntimeException {
4+
5+
MyServiceException(String msg) {
6+
super(msg);
7+
}
8+
9+
}

0 commit comments

Comments
 (0)