GET endpoint with Integration Tests
parent
74debb4b4c
commit
d0fd928ef7
4
.obsidian/app.json
vendored
Normal file
4
.obsidian/app.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"alwaysUpdateLinks": true,
|
||||||
|
"attachmentFolderPath": "assets"
|
||||||
|
}
|
3
.obsidian/appearance.json
vendored
Normal file
3
.obsidian/appearance.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"accentColor": ""
|
||||||
|
}
|
29
.obsidian/core-plugins-migration.json
vendored
Normal file
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
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
1
.obsidian/hotkeys.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
192
.obsidian/workspace.json
vendored
Normal file
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
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
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 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
|
||||||
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 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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
96
IntegrationTests.md
Normal file
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) ==it’s 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`.
|
BIN
assets/Pasted image 20230719102322.png
Normal file
BIN
assets/Pasted image 20230719102322.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 141 KiB |
Loading…
Reference in New Issue
Block a user