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")
|
||||
|
||||
## 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
|
||||
}
|
||||
```
|
||||
|
||||
|
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