From 757fd57c38f39666d8cd7339cfe306e464bb70f0 Mon Sep 17 00:00:00 2001 From: Kai Moritz Date: Sun, 27 Sep 2020 11:23:03 +0200 Subject: [PATCH] Further refined the example: Simplified the setup * Goal: Do not rely on knowledge about Thymeleaf * Goal: Separate MVC (controller) from business logic (service) * Introduced an ExampleService * The service checks the answer for The Ultimate Question Of Life, The Universe And Everything * Requests without any answer are tolarated, so that the page with the question can be rendered * Only numbers (Integer) are allowed as answer. * Negative numbers are not allowed as answers: the service answers with an empty Optional. * The view contains a bug, that results in a 503 for negative answers: Optional.get() is called, regardless of the state of the Optional * The view renders the question, if no answer is specified (initial request!) * If a valid request is specified, the answer and the outcome is rendered * If an invalid request (negative number!) is specified, the view generates an exception, because it tries to resolve the Optional anyway --- .../java/de/juplo/demo/ExampleController.java | 47 ++++++++++++--- .../java/de/juplo/demo/ExampleService.java | 20 +++++++ src/main/resources/templates/a.html | 28 --------- src/main/resources/templates/b.html | 28 --------- src/main/resources/templates/view.html | 28 +++++++++ .../ExceptionHandlingApplicationTests.java | 60 +++++++++++++++---- 6 files changed, 136 insertions(+), 75 deletions(-) create mode 100644 src/main/java/de/juplo/demo/ExampleService.java delete mode 100644 src/main/resources/templates/a.html delete mode 100644 src/main/resources/templates/b.html create mode 100644 src/main/resources/templates/view.html diff --git a/src/main/java/de/juplo/demo/ExampleController.java b/src/main/java/de/juplo/demo/ExampleController.java index b6e75d8..d919b63 100644 --- a/src/main/java/de/juplo/demo/ExampleController.java +++ b/src/main/java/de/juplo/demo/ExampleController.java @@ -9,7 +9,9 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; -import org.thymeleaf.exceptions.TemplateInputException; +import org.thymeleaf.exceptions.TemplateProcessingException; + +import java.util.Optional; @Controller public class ExampleController @@ -18,21 +20,50 @@ public class ExampleController LoggerFactory.getLogger(ExampleController.class); + private final ExampleService service; + + + public ExampleController(ExampleService service) + { + this.service = service; + } + + @RequestMapping("/") public String controller( - @RequestParam(defaultValue = "a") String template, + @RequestParam(required = false) Integer answer, Model model ) { - model.addAttribute("template", template); - return template; + Optional outcome = + answer == null ? null : service.checkAnswer(answer); + + model.addAttribute("answer", answer); + model.addAttribute("outcome", outcome); + return "view"; } - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - @ExceptionHandler(TemplateInputException.class) - public void templateInputException(TemplateInputException e) + /** + * This {@link ExceptionHandler @ExceptionHander} is never triggered, + * because the exception is not thrown inside the controller: + * It is functionless! + *

+ * The exception is thrown by Thymeleaf, which is called by the + * {@link DispatcherServlet} after the controller has finished its + * work during the rendering of the outcome. + *

+ *

+ * {@link ExceptionHandler @ExceptionHander's} are not able, to catch + * and resolve exceptions, which are thrown outside of the scope of the + * controller. + * This is also true for {@link ControllerAdvice}. + *

+ */ + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(TemplateProcessingException.class) + public void templateInputException(TemplateProcessingException e) { - LOG.error("{}: {}", HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + LOG.error("{}: {}", HttpStatus.BAD_REQUEST, e.getMessage()); } } diff --git a/src/main/java/de/juplo/demo/ExampleService.java b/src/main/java/de/juplo/demo/ExampleService.java new file mode 100644 index 0000000..ec21629 --- /dev/null +++ b/src/main/java/de/juplo/demo/ExampleService.java @@ -0,0 +1,20 @@ +package de.juplo.demo; + +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +public class ExampleService +{ + public Optional checkAnswer(int answer) + { + if (answer < 0) + return Optional.empty(); + + if (answer == 42) + return Optional.of(true); + else + return Optional.of(false); + } +} diff --git a/src/main/resources/templates/a.html b/src/main/resources/templates/a.html deleted file mode 100644 index 60de028..0000000 --- a/src/main/resources/templates/a.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - Testing Exception-Handling - Template A - - - -

Template A

-
-

TEXT

-

- Type in a or b - for an existing template (no exception). Type in any other string for a - non-existent template: Thymeleaf will throw a - TemplateInputException! -

-
-
-
-
- - -
- -
-
- - diff --git a/src/main/resources/templates/b.html b/src/main/resources/templates/b.html deleted file mode 100644 index c89c69f..0000000 --- a/src/main/resources/templates/b.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - Testing Exception-Handling - Template B - - - -

Template B

-
-

TEXT

-

- Type in a or b - for an existing template (no exception). Type in any other string for a - non-existent template: Thymeleaf will throw a - TemplateInputException! -

-
-
-
-
- - -
- -
-
- - diff --git a/src/main/resources/templates/view.html b/src/main/resources/templates/view.html new file mode 100644 index 0000000..70d3c51 --- /dev/null +++ b/src/main/resources/templates/view.html @@ -0,0 +1,28 @@ + + + + Template: view + + + +

Deep Thought

+
+

+ What is the answer to the ultimate question of life, the universe and everything? +

+
    +
  • Presented answer: ANSWER
  • +
  • Outcome: OUTCOME
  • +
+
+
+
+
+ + + +
+
+
+ + diff --git a/src/test/java/de/juplo/demo/ExceptionHandlingApplicationTests.java b/src/test/java/de/juplo/demo/ExceptionHandlingApplicationTests.java index 9e6d753..a80ffd3 100644 --- a/src/test/java/de/juplo/demo/ExceptionHandlingApplicationTests.java +++ b/src/test/java/de/juplo/demo/ExceptionHandlingApplicationTests.java @@ -1,14 +1,22 @@ package de.juplo.demo; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; import java.net.URI; +import java.util.Optional; +import static org.mockito.AdditionalMatchers.geq; +import static org.mockito.AdditionalMatchers.lt; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -17,6 +25,9 @@ class ExceptionHandlingApplicationTests { private final static Logger LOG = LoggerFactory.getLogger(ExceptionHandlingApplicationTests.class); + @MockBean + ExampleService service; + @Autowired MockMvc mvc; @@ -26,26 +37,53 @@ class ExceptionHandlingApplicationTests { } @Test - void test200() throws Exception { + void test200ForNoAnswer() throws Exception { mvc - .perform(get(URI.create("http://FOO/?template=a"))) + .perform(get(URI.create("http://FOO/"))) .andExpect(status().isOk()); + + verify(service, times(0)).checkAnswer(anyInt()); + } + + @ParameterizedTest + @ValueSource(ints = { 0, 1, 2, 3, 4, 5, 41, 42, 43, 666, Integer.MAX_VALUE }) + void test200ForPositiveAnswer(int number) throws Exception { + when(service.checkAnswer(eq(42))).thenReturn(Optional.of(true)); + when(service.checkAnswer(geq(0))).thenReturn(Optional.of(false)); + when(service.checkAnswer(lt(0))).thenReturn(Optional.empty()); + mvc - .perform(get(URI.create("http://FOO/?template=b"))) + .perform(get(URI.create("http://FOO/?answer=" + number))) .andExpect(status().isOk()); } - @Test - void test503_NOT_WORKING() throws Exception { - // The expected behaviour of the following test is, that the - // TemplateInputException, that is thrown by Thymeleaf because of the non-existent - // template-resource, is catched and reported as 503 Internal Server Error, as it - // happens in the real Spring-MVC environment, when you start the web-server and - // trigger the error manually. + @ParameterizedTest + @ValueSource(ints = { -1, -2, Integer.MIN_VALUE }) + void test400ForNegativeAnswer_NOT_WORKING(int number) throws Exception { + when(service.checkAnswer(eq(42))).thenReturn(Optional.of(true)); + when(service.checkAnswer(geq(0))).thenReturn(Optional.of(false)); + when(service.checkAnswer(lt(0))).thenReturn(Optional.empty()); + + // The expected behaviour of the following test is, that the NoSuchElementException + // with the message "No value present", that is raised, when the view calls .get() + // on the empty Optional and wrapped by Thymeleaf in a TemplateProcessingException + // is catched by the @ExceptionHandler, that is defined in the ExampleController + // and reported as 400: Bad Request. // Instead, the exception bubbles up, becomes wrapped in a NestedServletException // and is thrown in the call to perform()! mvc - .perform(get(URI.create("http://FOO/?template=foo"))) + .perform(get(URI.create("http://FOO/?answer=" + number))) .andExpect(status().isInternalServerError()); } + + @Test + void test400ForStringInput() throws Exception { + when(service.checkAnswer(eq(42))).thenReturn(Optional.of(true)); + when(service.checkAnswer(geq(0))).thenReturn(Optional.of(false)); + when(service.checkAnswer(lt(0))).thenReturn(Optional.empty()); + + mvc + .perform(get(URI.create("http://FOO/?answer=bar"))) + .andExpect(status().isBadRequest()); + } } -- 2.20.1