Feature/bael 6083 http interface (#13251)
* BAEL-6083: Update Spring Boot, initial tests * BAEL-6083: Fix tests * BAEL-6083: Add post method * BAEL-6083: Failing POST test * BAEL-6083: Remove POST test * BAEL-6083: Fix POST tests * BAEL-6083: Add delete method * BAEL-6083: Add id field * BAEL-6083: Exception handling * BAEL-6083: Refactor * BAEL-6083: Use Java 17 * BAEL-6083: Update Mockito to use BDD and deep mocks * BAEL-6083: Unused vars
This commit is contained in:
parent
185093c26a
commit
35fd9e473a
|
@ -10,27 +10,39 @@
|
|||
<url>http://www.baeldung.com</url>
|
||||
|
||||
<parent>
|
||||
<groupId>com.baeldung</groupId>
|
||||
<artifactId>parent-modules</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.0.1</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mock-server</groupId>
|
||||
<artifactId>mockserver-netty</artifactId>
|
||||
<version>${mockserver.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mock-server</groupId>
|
||||
<artifactId>mockserver-client-java</artifactId>
|
||||
<version>${mockserver.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit-jupiter.version}</version>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
@ -76,13 +88,23 @@
|
|||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<spring.boot.version>2.7.5</spring.boot.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<mockserver.version>5.14.0</mockserver.version>
|
||||
</properties>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,3 @@
|
|||
package com.baeldung.httpinterface;
|
||||
|
||||
public record Book(long id, String title, String author, int year) {}
|
|
@ -0,0 +1,23 @@
|
|||
package com.baeldung.httpinterface;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
|
||||
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
|
||||
|
||||
@Component
|
||||
public class BooksClient {
|
||||
|
||||
private final BooksService booksService;
|
||||
|
||||
public BooksClient(WebClient webClient) {
|
||||
HttpServiceProxyFactory httpServiceProxyFactory =
|
||||
HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient))
|
||||
.build();
|
||||
booksService = httpServiceProxyFactory.createClient(BooksService.class);
|
||||
}
|
||||
|
||||
public BooksService getBooksService() {
|
||||
return booksService;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.baeldung.httpinterface;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.service.annotation.DeleteExchange;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
import org.springframework.web.service.annotation.PostExchange;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
interface BooksService {
|
||||
|
||||
@GetExchange("/books")
|
||||
List<Book> getBooks();
|
||||
|
||||
@GetExchange("/books/{id}")
|
||||
Book getBook(@PathVariable long id);
|
||||
|
||||
@PostExchange("/books")
|
||||
Book saveBook(@RequestBody Book book);
|
||||
|
||||
@DeleteExchange("/books/{id}")
|
||||
ResponseEntity<Void> deleteBook(@PathVariable long id);
|
||||
|
||||
}
|
|
@ -5,7 +5,6 @@ import org.apache.commons.logging.LogFactory;
|
|||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
package com.baeldung.httpinterface;
|
||||
|
||||
import org.apache.http.HttpException;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockserver.client.MockServerClient;
|
||||
import org.mockserver.integration.ClientAndServer;
|
||||
import org.mockserver.configuration.Configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.util.List;
|
||||
|
||||
import org.mockserver.model.HttpRequest;
|
||||
import org.mockserver.model.MediaType;
|
||||
import org.mockserver.verify.VerificationTimes;
|
||||
import org.slf4j.event.Level;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockserver.integration.ClientAndServer.startClientAndServer;
|
||||
import static org.mockserver.matchers.Times.exactly;
|
||||
import static org.mockserver.model.HttpRequest.request;
|
||||
import static org.mockserver.model.HttpResponse.response;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class BooksServiceMockServerTest {
|
||||
|
||||
private static final String SERVER_ADDRESS = "localhost";
|
||||
private static final String PATH = "/books";
|
||||
|
||||
private static int serverPort;
|
||||
private static ClientAndServer mockServer;
|
||||
private static String serviceUrl;
|
||||
|
||||
@BeforeAll
|
||||
static void startServer() throws IOException {
|
||||
serverPort = getFreePort();
|
||||
serviceUrl = "http://" + SERVER_ADDRESS + ":" + serverPort;
|
||||
|
||||
Configuration config = Configuration.configuration().logLevel(Level.WARN);
|
||||
mockServer = startClientAndServer(config, serverPort);
|
||||
|
||||
mockAllBooksRequest();
|
||||
mockBookByIdRequest();
|
||||
mockSaveBookRequest();
|
||||
mockDeleteBookRequest();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopServer() {
|
||||
mockServer.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenMockedGetResponse_whenGetBooksServiceMethodIsCalled_thenTwoBooksAreReturned() {
|
||||
BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
|
||||
BooksService booksService = booksClient.getBooksService();
|
||||
|
||||
List<Book> books = booksService.getBooks();
|
||||
assertEquals(2, books.size());
|
||||
|
||||
mockServer.verify(
|
||||
HttpRequest.request()
|
||||
.withMethod(HttpMethod.GET.name())
|
||||
.withPath(PATH),
|
||||
VerificationTimes.exactly(1)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenMockedGetResponse_whenGetExistingBookServiceMethodIsCalled_thenCorrectBookIsReturned() {
|
||||
BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
|
||||
BooksService booksService = booksClient.getBooksService();
|
||||
|
||||
Book book = booksService.getBook(1);
|
||||
assertEquals("Book_1", book.title());
|
||||
|
||||
mockServer.verify(
|
||||
HttpRequest.request()
|
||||
.withMethod(HttpMethod.GET.name())
|
||||
.withPath(PATH + "/1"),
|
||||
VerificationTimes.exactly(1)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenMockedGetResponse_whenGetNonExistingBookServiceMethodIsCalled_thenCorrectBookIsReturned() {
|
||||
BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
|
||||
BooksService booksService = booksClient.getBooksService();
|
||||
|
||||
assertThrows(WebClientResponseException.class, () -> booksService.getBook(9));
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenCustomErrorHandlerIsSet_whenGetNonExistingBookServiceMethodIsCalled_thenCustomExceptionIsThrown() {
|
||||
BooksClient booksClient = new BooksClient(WebClient.builder()
|
||||
.defaultStatusHandler(HttpStatusCode::isError, resp ->
|
||||
Mono.just(new MyServiceException("Custom exception")))
|
||||
.baseUrl(serviceUrl)
|
||||
.build());
|
||||
|
||||
BooksService booksService = booksClient.getBooksService();
|
||||
assertThrows(MyServiceException.class, () -> booksService.getBook(9));
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenMockedPostResponse_whenSaveBookServiceMethodIsCalled_thenCorrectBookIsReturned() {
|
||||
BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
|
||||
BooksService booksService = booksClient.getBooksService();
|
||||
|
||||
Book book = booksService.saveBook(new Book(3, "Book_3", "Author_3", 2000));
|
||||
assertEquals("Book_3", book.title());
|
||||
|
||||
mockServer.verify(
|
||||
HttpRequest.request()
|
||||
.withMethod(HttpMethod.POST.name())
|
||||
.withPath(PATH),
|
||||
VerificationTimes.exactly(1)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenMockedDeleteResponse_whenDeleteBookServiceMethodIsCalled_thenCorrectCodeIsReturned() {
|
||||
BooksClient booksClient = new BooksClient(WebClient.builder().baseUrl(serviceUrl).build());
|
||||
BooksService booksService = booksClient.getBooksService();
|
||||
|
||||
ResponseEntity<Void> response = booksService.deleteBook(3);
|
||||
assertEquals(HttpStatusCode.valueOf(200), response.getStatusCode());
|
||||
|
||||
mockServer.verify(
|
||||
HttpRequest.request()
|
||||
.withMethod(HttpMethod.DELETE.name())
|
||||
.withPath(PATH + "/3"),
|
||||
VerificationTimes.exactly(1)
|
||||
);
|
||||
}
|
||||
|
||||
private static int getFreePort () throws IOException {
|
||||
try (ServerSocket serverSocket = new ServerSocket(0)) {
|
||||
return serverSocket.getLocalPort();
|
||||
}
|
||||
}
|
||||
|
||||
private static void mockAllBooksRequest() {
|
||||
new MockServerClient(SERVER_ADDRESS, serverPort)
|
||||
.when(
|
||||
request()
|
||||
.withPath(PATH)
|
||||
.withMethod(HttpMethod.GET.name()),
|
||||
exactly(1)
|
||||
)
|
||||
.respond(
|
||||
response()
|
||||
.withStatusCode(HttpStatus.SC_OK)
|
||||
.withContentType(MediaType.APPLICATION_JSON)
|
||||
.withBody("[{\"id\":1,\"title\":\"Book_1\",\"author\":\"Author_1\",\"year\":1998},{\"id\":2,\"title\":\"Book_2\",\"author\":\"Author_2\",\"year\":1999}]")
|
||||
);
|
||||
}
|
||||
|
||||
private static void mockBookByIdRequest() {
|
||||
new MockServerClient(SERVER_ADDRESS, serverPort)
|
||||
.when(
|
||||
request()
|
||||
.withPath(PATH + "/1")
|
||||
.withMethod(HttpMethod.GET.name()),
|
||||
exactly(1)
|
||||
)
|
||||
.respond(
|
||||
response()
|
||||
.withStatusCode(HttpStatus.SC_OK)
|
||||
.withContentType(MediaType.APPLICATION_JSON)
|
||||
.withBody("{\"id\":1,\"title\":\"Book_1\",\"author\":\"Author_1\",\"year\":1998}")
|
||||
);
|
||||
}
|
||||
|
||||
private static void mockSaveBookRequest() {
|
||||
new MockServerClient(SERVER_ADDRESS, serverPort)
|
||||
.when(
|
||||
request()
|
||||
.withPath(PATH)
|
||||
.withMethod(HttpMethod.POST.name())
|
||||
.withContentType(MediaType.APPLICATION_JSON)
|
||||
.withBody("{\"id\":3,\"title\":\"Book_3\",\"author\":\"Author_3\",\"year\":2000}"),
|
||||
exactly(1)
|
||||
)
|
||||
.respond(
|
||||
response()
|
||||
.withStatusCode(HttpStatus.SC_OK)
|
||||
.withContentType(MediaType.APPLICATION_JSON)
|
||||
.withBody("{\"id\":3,\"title\":\"Book_3\",\"author\":\"Author_3\",\"year\":2000}")
|
||||
);
|
||||
}
|
||||
|
||||
private static void mockDeleteBookRequest() {
|
||||
new MockServerClient(SERVER_ADDRESS, serverPort)
|
||||
.when(
|
||||
request()
|
||||
.withPath(PATH + "/3")
|
||||
.withMethod(HttpMethod.DELETE.name()),
|
||||
exactly(1)
|
||||
)
|
||||
.respond(
|
||||
response()
|
||||
.withStatusCode(HttpStatus.SC_OK)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package com.baeldung.httpinterface;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.mockito.BDDMockito.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BooksServiceMockitoTest {
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private WebClient webClient;
|
||||
|
||||
@InjectMocks
|
||||
private BooksClient booksClient;
|
||||
|
||||
@Test
|
||||
void givenMockedWebClientReturnsTwoBooks_whenGetBooksServiceMethodIsCalled_thenListOfTwoBooksIsReturned() {
|
||||
given(webClient.method(HttpMethod.GET)
|
||||
.uri(anyString(), anyMap())
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<List<Book>>(){}))
|
||||
.willReturn(Mono.just(List.of(
|
||||
new Book(1,"Book_1", "Author_1", 1998),
|
||||
new Book(2, "Book_2", "Author_2", 1999)
|
||||
)));
|
||||
|
||||
BooksService booksService = booksClient.getBooksService();
|
||||
List<Book> books = booksService.getBooks();
|
||||
assertEquals(2, books.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenMockedWebClientReturnsBook_whenGetBookServiceMethodIsCalled_thenBookIsReturned() {
|
||||
given(webClient.method(HttpMethod.GET)
|
||||
.uri(anyString(), anyMap())
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<Book>(){}))
|
||||
.willReturn(Mono.just(new Book(1,"Book_1", "Author_1", 1998)));
|
||||
|
||||
BooksService booksService = booksClient.getBooksService();
|
||||
Book book = booksService.getBook(1);
|
||||
assertEquals("Book_1", book.title());
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenMockedWebClientReturnsBook_whenSaveBookServiceMethodIsCalled_thenBookIsReturned() {
|
||||
given(webClient.method(HttpMethod.POST)
|
||||
.uri(anyString(), anyMap())
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<Book>(){}))
|
||||
.willReturn(Mono.just(new Book(3, "Book_3", "Author_3", 2000)));
|
||||
|
||||
BooksService booksService = booksClient.getBooksService();
|
||||
Book book = booksService.saveBook(new Book(3, "Book_3", "Author_3", 2000));
|
||||
assertEquals("Book_3", book.title());
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenMockedWebClientReturnsOk_whenDeleteBookServiceMethodIsCalled_thenOkCodeIsReturned() {
|
||||
given(webClient.method(HttpMethod.DELETE)
|
||||
.uri(anyString(), anyMap())
|
||||
.retrieve()
|
||||
.toBodilessEntity()
|
||||
.block(any())
|
||||
.getStatusCode())
|
||||
.willReturn(HttpStatusCode.valueOf(200));
|
||||
|
||||
BooksService booksService = booksClient.getBooksService();
|
||||
ResponseEntity<Void> response = booksService.deleteBook(3);
|
||||
assertEquals(HttpStatusCode.valueOf(200), response.getStatusCode());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.baeldung.httpinterface;
|
||||
|
||||
public class MyServiceException extends RuntimeException {
|
||||
|
||||
MyServiceException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue