GET endpoint with Integration Tests

djmil 2023-07-19 13:58:44 +02:00
parent 74debb4b4c
commit d0fd928ef7
10 changed files with 451 additions and 2 deletions

4
.obsidian/app.json vendored Normal file

@ -0,0 +1,4 @@
{
"alwaysUpdateLinks": true,
"attachmentFolderPath": "assets"
}

3
.obsidian/appearance.json vendored Normal file

@ -0,0 +1,3 @@
{
"accentColor": ""
}

29
.obsidian/core-plugins-migration.json vendored Normal file

@ -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
}

20
.obsidian/core-plugins.json vendored Normal file

@ -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"
]

1
.obsidian/hotkeys.json vendored Normal file

@ -0,0 +1 @@
{}

192
.obsidian/workspace.json vendored Normal file

@ -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"
]
}

52
GET.md Normal file

@ -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<CashCard> 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<String> 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]].

56
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") ![Testing pyramid](https://blog.missiondata.com/wp-content/uploads/MD_TestingPyramid2x-1560x1045.png "Testing pyramid")
## Unit Tests ## 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 theyre 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 theyre key to designing highly cohesive, loosely coupled software.
## Integration Tests ## 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 ## 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. 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._ 3. **Refactor:** Look for opportunities to simplify, reduce duplication, or otherwise improve the code without changing any behavior—to _refactor._
4. Repeat! 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 Springs *Component Scan* phase. This happens at application startup. The Bean is stored in Springs `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 {
}
```
Thats 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
}
```

96
IntegrationTests.md Normal file

@ -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<String> 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) ==its best used only in tests==. We'll discuss this in more detail later.
## Perform actual `Get` request
```java
@Test
void shouldReturnACashCardWhenDataIsSaved() {
ResponseEntity<String> 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`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB