From 35fd9e473a6b8c0fb3007f241067337f8d0c0c2e Mon Sep 17 00:00:00 2001 From: Daniel Strmecki Date: Sat, 28 Jan 2023 12:01:25 +0100 Subject: [PATCH] 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 --- spring-core-6/pom.xml | 44 +++- .../java/com/baeldung/httpinterface/Book.java | 3 + .../baeldung/httpinterface/BooksClient.java | 23 ++ .../baeldung/httpinterface/BooksService.java | 26 +++ .../reinitializebean/cache/ConfigManager.java | 1 - .../BooksServiceMockServerTest.java | 217 ++++++++++++++++++ .../BooksServiceMockitoTest.java | 88 +++++++ .../httpinterface/MyServiceException.java | 9 + 8 files changed, 399 insertions(+), 12 deletions(-) create mode 100644 spring-core-6/src/main/java/com/baeldung/httpinterface/Book.java create mode 100644 spring-core-6/src/main/java/com/baeldung/httpinterface/BooksClient.java create mode 100644 spring-core-6/src/main/java/com/baeldung/httpinterface/BooksService.java create mode 100644 spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockServerTest.java create mode 100644 spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockitoTest.java create mode 100644 spring-core-6/src/test/java/com/baeldung/httpinterface/MyServiceException.java diff --git a/spring-core-6/pom.xml b/spring-core-6/pom.xml index 2df7167ca1..a3dda0374f 100644 --- a/spring-core-6/pom.xml +++ b/spring-core-6/pom.xml @@ -10,27 +10,39 @@ http://www.baeldung.com - com.baeldung - parent-modules - 1.0.0-SNAPSHOT + org.springframework.boot + spring-boot-starter-parent + 3.0.1 + org.springframework.boot spring-boot-starter-web - ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-webflux + + + org.mock-server + mockserver-netty + ${mockserver.version} + + + org.mock-server + mockserver-client-java + ${mockserver.version} org.springframework.boot spring-boot-starter-test - ${spring.boot.version} test - org.junit.jupiter - junit-jupiter-api - ${junit-jupiter.version} + io.projectreactor + reactor-test test @@ -76,13 +88,23 @@ + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + UTF-8 - 11 - 11 - 2.7.5 + 17 + 17 + 5.14.0 \ No newline at end of file diff --git a/spring-core-6/src/main/java/com/baeldung/httpinterface/Book.java b/spring-core-6/src/main/java/com/baeldung/httpinterface/Book.java new file mode 100644 index 0000000000..a38085852e --- /dev/null +++ b/spring-core-6/src/main/java/com/baeldung/httpinterface/Book.java @@ -0,0 +1,3 @@ +package com.baeldung.httpinterface; + +public record Book(long id, String title, String author, int year) {} diff --git a/spring-core-6/src/main/java/com/baeldung/httpinterface/BooksClient.java b/spring-core-6/src/main/java/com/baeldung/httpinterface/BooksClient.java new file mode 100644 index 0000000000..3034f4f528 --- /dev/null +++ b/spring-core-6/src/main/java/com/baeldung/httpinterface/BooksClient.java @@ -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; + } +} diff --git a/spring-core-6/src/main/java/com/baeldung/httpinterface/BooksService.java b/spring-core-6/src/main/java/com/baeldung/httpinterface/BooksService.java new file mode 100644 index 0000000000..a9cf6ec58a --- /dev/null +++ b/spring-core-6/src/main/java/com/baeldung/httpinterface/BooksService.java @@ -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 getBooks(); + + @GetExchange("/books/{id}") + Book getBook(@PathVariable long id); + + @PostExchange("/books") + Book saveBook(@RequestBody Book book); + + @DeleteExchange("/books/{id}") + ResponseEntity deleteBook(@PathVariable long id); + +} diff --git a/spring-core-6/src/main/java/com/baeldung/reinitializebean/cache/ConfigManager.java b/spring-core-6/src/main/java/com/baeldung/reinitializebean/cache/ConfigManager.java index 1e4dee6cc4..240fb350c2 100644 --- a/spring-core-6/src/main/java/com/baeldung/reinitializebean/cache/ConfigManager.java +++ b/spring-core-6/src/main/java/com/baeldung/reinitializebean/cache/ConfigManager.java @@ -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; diff --git a/spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockServerTest.java b/spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockServerTest.java new file mode 100644 index 0000000000..22e00c16ae --- /dev/null +++ b/spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockServerTest.java @@ -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 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 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) + ); + } + +} diff --git a/spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockitoTest.java b/spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockitoTest.java new file mode 100644 index 0000000000..7a82835ef3 --- /dev/null +++ b/spring-core-6/src/test/java/com/baeldung/httpinterface/BooksServiceMockitoTest.java @@ -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>(){})) + .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 books = booksService.getBooks(); + assertEquals(2, books.size()); + } + + @Test + void givenMockedWebClientReturnsBook_whenGetBookServiceMethodIsCalled_thenBookIsReturned() { + given(webClient.method(HttpMethod.GET) + .uri(anyString(), anyMap()) + .retrieve() + .bodyToMono(new ParameterizedTypeReference(){})) + .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(){})) + .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 response = booksService.deleteBook(3); + assertEquals(HttpStatusCode.valueOf(200), response.getStatusCode()); + } + +} diff --git a/spring-core-6/src/test/java/com/baeldung/httpinterface/MyServiceException.java b/spring-core-6/src/test/java/com/baeldung/httpinterface/MyServiceException.java new file mode 100644 index 0000000000..e09335a211 --- /dev/null +++ b/spring-core-6/src/test/java/com/baeldung/httpinterface/MyServiceException.java @@ -0,0 +1,9 @@ +package com.baeldung.httpinterface; + +public class MyServiceException extends RuntimeException { + + MyServiceException(String msg) { + super(msg); + } + +}