diff --git a/.obsidian/app.json b/.obsidian/app.json new file mode 100644 index 0000000..81a4620 --- /dev/null +++ b/.obsidian/app.json @@ -0,0 +1,4 @@ +{ + "alwaysUpdateLinks": true, + "attachmentFolderPath": "assets" +} \ No newline at end of file diff --git a/.obsidian/appearance.json b/.obsidian/appearance.json new file mode 100644 index 0000000..c8c365d --- /dev/null +++ b/.obsidian/appearance.json @@ -0,0 +1,3 @@ +{ + "accentColor": "" +} \ No newline at end of file diff --git a/.obsidian/core-plugins-migration.json b/.obsidian/core-plugins-migration.json new file mode 100644 index 0000000..5c13490 --- /dev/null +++ b/.obsidian/core-plugins-migration.json @@ -0,0 +1,29 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": false +} \ No newline at end of file diff --git a/.obsidian/core-plugins.json b/.obsidian/core-plugins.json new file mode 100644 index 0000000..9405bfd --- /dev/null +++ b/.obsidian/core-plugins.json @@ -0,0 +1,20 @@ +[ + "file-explorer", + "global-search", + "switcher", + "graph", + "backlink", + "canvas", + "outgoing-link", + "tag-pane", + "page-preview", + "daily-notes", + "templates", + "note-composer", + "command-palette", + "editor-status", + "bookmarks", + "outline", + "word-count", + "file-recovery" +] \ No newline at end of file diff --git a/.obsidian/hotkeys.json b/.obsidian/hotkeys.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.obsidian/hotkeys.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json new file mode 100644 index 0000000..21ce55e --- /dev/null +++ b/.obsidian/workspace.json @@ -0,0 +1,192 @@ +{ + "main": { + "id": "bfa6b0eb76264f17", + "type": "split", + "children": [ + { + "id": "91a95d9b9ba4dc49", + "type": "tabs", + "children": [ + { + "id": "a3660fdf5f5aceb3", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "Home.md", + "mode": "source", + "source": false + } + } + }, + { + "id": "7e5ec70badaa86f3", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "IntegrationTests.md", + "mode": "source", + "source": false + } + } + }, + { + "id": "ec5879550528c04c", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "GET.md", + "mode": "source", + "source": false + } + } + } + ], + "currentTab": 2 + } + ], + "direction": "vertical" + }, + "left": { + "id": "5eaa206d99edb028", + "type": "split", + "children": [ + { + "id": "d007c720b5e598a0", + "type": "tabs", + "children": [ + { + "id": "03171174265e0ee5", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical" + } + } + }, + { + "id": "f78dd3c57a1694ca", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + } + } + }, + { + "id": "ef97ee1190cc8880", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {} + } + } + ] + } + ], + "direction": "horizontal", + "width": 249.5 + }, + "right": { + "id": "3bd78c465d2a2030", + "type": "split", + "children": [ + { + "id": "e39fc11f28834b41", + "type": "tabs", + "children": [ + { + "id": "e0d70c2f91ae0cfb", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "file": "GET.md", + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + } + } + }, + { + "id": "11cce84d7b97ddc8", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "file": "GET.md", + "linksCollapsed": false, + "unlinkedCollapsed": true + } + } + }, + { + "id": "f8f2ce3f2694f545", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true + } + } + }, + { + "id": "58d34025bb119389", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "file": "GET.md" + } + } + } + ], + "currentTab": 3 + } + ], + "direction": "horizontal", + "width": 300 + }, + "left-ribbon": { + "hiddenItems": { + "switcher:Open quick switcher": false, + "graph:Open graph view": false, + "canvas:Create new canvas": false, + "daily-notes:Open today's daily note": false, + "templates:Insert template": false, + "command-palette:Open command palette": false + } + }, + "active": "ec5879550528c04c", + "lastOpenFiles": [ + "IntegrationTests.md", + "Home.md", + "GET.md", + "UnitTests.md", + "Pasted image 20230719102301.png", + "assets/Pasted image 20230719102322.png", + "assets", + "Pasted image 20230718185450.png", + "testPage.md", + "subfolder/Note1.md", + "_Sidebar.md", + "tests%252FSubpage.md.-.md", + "subfolder.md", + "subfolder%5C%2Fp2.md", + "subfolder" + ] +} \ No newline at end of file diff --git a/GET.md b/GET.md new file mode 100644 index 0000000..63c7af5 --- /dev/null +++ b/GET.md @@ -0,0 +1,52 @@ +`src/main/java/djmil/cashcard/CashCardController.java` + +```java + +package example.cashcard; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class CashCardController { + @GetMapping("/cashcards/{requestedId}") + public ResponseEntity findById(@PathVariable Long requestedId) { + if (requestedId.equals(99L)) { + CashCard cashCard = new CashCard(99L, 123.45); + return ResponseEntity.ok(cashCard); + } else { + return ResponseEntity.notFound().build(); + } + } +} +``` + +# @GetMapping + +Needs the URI Path and tells Spring to route `GET` requests strictly to the `findById` method. + +## Alternative variant for providing URI mapping for `@RestConttroller` + +```java +@RestController +@RequestMapping("/cashcards") +public class CashCardController { + + @GetMapping("/{requestedId}") + public ResponseEntity findById() { +``` + +# @PathVariable + +Spring needs to know how to get the value of the `requestedId` parameter. This is done using the `@PathVariable` annotation. The fact that the parameter name matches the `{requestedId}` text *(URI Path)* within the `@GetMapping` parameter allows Spring to assign (inject) the correct value to the `requestedId` variable. + +# ResponseEntity + +REST says that the Response needs to contain a Cash Card in its body, and a Response code of 200 (OK). Spring Web provides the `ResponseEntity` class for this purpose. It also provides several utility methods to produce Response Entities. Here `ResponseEntity` used to create a Response with code **200 (OK)**, and a body containing a `CashCard`. + +# Testing + +Testing `GET` and other REST request is considerate to be [[IntegrationTests]]. \ No newline at end of file diff --git a/Home.md b/Home.md index be039e5..deb72a5 100644 --- a/Home.md +++ b/Home.md @@ -27,10 +27,10 @@ Different tests can be written at different levels of the system. At each level, ![Testing pyramid](https://blog.missiondata.com/wp-content/uploads/MD_TestingPyramid2x-1560x1045.png "Testing pyramid") ## Unit Tests -A [[UnitTests]] exercises a small “unit” of the system that is isolated from the rest of the system. They should be simple and speedy. You want a high ratio of Unit Tests in your testing pyramid as they’re key to designing highly cohesive, loosely coupled software. +[[UnitTests]] exercises a small “unit” of the system that is isolated from the rest of the system. They should be simple and speedy. You want a high ratio of Unit Tests in your testing pyramid as they’re key to designing highly cohesive, loosely coupled software. ## Integration Tests -Integration Tests exercise a subset of the system and may exercise groups of units in one test. They are more complicated to write and maintain, and run slower than unit tests. +[[IntegrationTests]] exercise a subset of the system and may exercise groups of units in one test. They are more complicated to write and maintain, and run slower than unit tests. ## End-to-End Tests An End-to-End Test exercises the system using the same interface that a user would, such as a web browser. While extremely thorough, End-to-End Tests can be very slow and fragile because they use simulated user interactions in potentially complicated UIs. Implement the smallest number of these tests. @@ -44,7 +44,59 @@ Software development teams love to move fast. So how do you go fast forever? By 3. **Refactor:** Look for opportunities to simplify, reduce duplication, or otherwise improve the code without changing any behavior—to _refactor._ 4. Repeat! +# RESTful API +In a RESTful system, data objects are called Resource Representations. The purpose of a RESTful API is to manage the state of these Resources. The chart below shows details about RESTful CRUD operations of an application. +|Operation|API Endpoint|HTTP Method|Response Status| +|---|---|---|---| +|**C**reate|`/cashcards`|`POST`|201 (CREATED)| +|**R**ead|`/cashcards/{id}`|`GET`|200 (OK)| +|**U**pdate|`/cashcards/{id}`|`PUT`|204 (NO DATA)| +|**D**elete|`/cashcards/{id}`|`DELETE`|204 (NO DATA)| +Another common concept associated with REST is the Hypertext Transfer Protocol. In **HTTP**, a caller sends a Request to a URI. A web server receives the request, and routes it to a request handler. The handler creates a Response, which is then sent back to the caller. + +## REST in Spring Boot + +One of the main things Spring does is to configure and instantiate objects. These objects are called *Spring Beans*, and are usually created by Spring (as opposed to using the Java `new` keyword). You can direct Spring to create Beans in several ways. + +> We will annotate a class with a Spring Annotation, which directs Spring to create an instance of the class during Spring’s *Component Scan* phase. This happens at application startup. The Bean is stored in Spring’s `IoC Container`. From here, the bean can be injected into any code that requests it. + +![[Pasted image 20230719102322.png]] + +## @RestController + +In Spring Web, Requests are handled by Controllers. We are going to use the more specific `RestController` annotation. The actual class shall be placed in `src/main/java/djmil/cashcard/CashCardController.java` + +```java +@RestController +public class CashCardController { +} +``` + +That’s all it takes to tell Spring: “create a REST Controller”. The Controller gets injected into Spring Web, which routes API requests (handled by the Controller) with help of [[GET#@GetMapping]] annotation to the correct method. + +## Get +**** +In [[GET]] requests, the body is empty. So, the request to read the Cash Card with an id of 123 would be: + +``` +Request: + Method: GET + URL: http://cashcard.example.com/cashcards/123 + Body: (empty) +``` + +The response to a successful Read request has a body containing the JSON representation of the Resource which was requested, with a Response Status Code of 200 (OK). So the response to the above Read request would look like this: + +``` +Response: + Status Code: 200 + Body: + { + "id": 123, + "amount": 25.00 + } +``` diff --git a/IntegrationTests.md b/IntegrationTests.md new file mode 100644 index 0000000..ebb84c8 --- /dev/null +++ b/IntegrationTests.md @@ -0,0 +1,96 @@ +It is expected that at this point you've worked thru [[Home#RESTful API]] section of a tutorial. + +Update `src/test/java/djmil/cashcard/CashCardApplicationTests.java +with this Integration Test: + +```java +package djmil.cashcard; + +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class CashCardApplicationTests { + @Autowired + TestRestTemplate restTemplate; + + @Test + void shouldReturnACashCardWhenDataIsSaved() { + ResponseEntity response = restTemplate.getForEntity("/cashcards/99", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + DocumentContext documentContext = JsonPath.parse(response.getBody()); + Number id = documentContext.read("$.id"); + assertThat(id).isNotNull(); + assertThat(id).isEqualTo(99); + } +} +``` + +# Mock an SpringBoot application + +```java +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class CashCardApplicationTests { +``` + +A `@SpringBootTest` annotation is a way to start our Spring Boot application and make it available for our tests to perform requests to it. + +# A way to perform HTTP requests + +## HTTP helper + +```java +class CashCardApplicationTests { + @Autowired + TestRestTemplate restTemplate; +``` + +[TestRestTemplate](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/web/client/TestRestTemplate.html) can be used to make HTTP requests to the locally running *(web)* application. + +**_Note_** that while [[UnitTests#@Autowired]] annotation is a convenient form of [Spring dependency injection](https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html) ==it’s best used only in tests==. We'll discuss this in more detail later. + +## Perform actual `Get` request + +```java +@Test +void shouldReturnACashCardWhenDataIsSaved() { + ResponseEntity response = restTemplate.getForEntity("/cashcards/99", String.class); +``` + +Here we use `restTemplate` to make an HTTP `GET` request to the application endpoint `/cashcards/99`. + +`restTemplate` will return a [[GET#ResponseEntity]], which we've captured in a variable we've named `response`. + +## Additional validators + +Now, we can (and shall) validate various aspects of expected response message. + +### HTTP Response Status code + +```java +assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); +``` + +### Response Body + +```java +DocumentContext documentContext = JsonPath.parse(response.getBody()); +``` + +Converts the response String into a JSON-aware object with lots of helper methods. +```java +Number id = documentContext.read("$.id"); +assertThat(id).isNotNull(); +``` + +We expect that when we request a Cash Card with `id` of `99` a JSON object will be returned with _something_ in the `id` field. For now assert that the `id` is not `null`. \ No newline at end of file diff --git a/assets/Pasted image 20230719102322.png b/assets/Pasted image 20230719102322.png new file mode 100644 index 0000000..0391d05 Binary files /dev/null and b/assets/Pasted image 20230719102322.png differ