database
parent
997c67f20a
commit
d35729d123
22
.obsidian/workspace.json
vendored
22
.obsidian/workspace.json
vendored
@ -25,26 +25,26 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "IntegrationTests.md",
|
"file": "Database.md",
|
||||||
"mode": "source",
|
"mode": "source",
|
||||||
"source": false
|
"source": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ec5879550528c04c",
|
"id": "af9b076d6f1ac377",
|
||||||
"type": "leaf",
|
"type": "leaf",
|
||||||
"state": {
|
"state": {
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "GET.md",
|
"file": "Database.md",
|
||||||
"mode": "source",
|
"mode": "source",
|
||||||
"source": false
|
"source": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"currentTab": 2
|
"currentTab": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"direction": "vertical"
|
"direction": "vertical"
|
||||||
@ -110,7 +110,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "backlink",
|
"type": "backlink",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "GET.md",
|
"file": "Database.md",
|
||||||
"collapseAll": false,
|
"collapseAll": false,
|
||||||
"extraContext": false,
|
"extraContext": false,
|
||||||
"sortOrder": "alphabetical",
|
"sortOrder": "alphabetical",
|
||||||
@ -127,7 +127,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "outgoing-link",
|
"type": "outgoing-link",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "GET.md",
|
"file": "Database.md",
|
||||||
"linksCollapsed": false,
|
"linksCollapsed": false,
|
||||||
"unlinkedCollapsed": true
|
"unlinkedCollapsed": true
|
||||||
}
|
}
|
||||||
@ -150,7 +150,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "outline",
|
"type": "outline",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "GET.md"
|
"file": "Database.md"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,12 +171,14 @@
|
|||||||
"command-palette:Open command palette": false
|
"command-palette:Open command palette": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"active": "ec5879550528c04c",
|
"active": "7e5ec70badaa86f3",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
"IntegrationTests.md",
|
"IntegrationTests.md",
|
||||||
"Home.md",
|
|
||||||
"GET.md",
|
|
||||||
"UnitTests.md",
|
"UnitTests.md",
|
||||||
|
"Database.md",
|
||||||
|
"Home.md",
|
||||||
|
"assets/Pasted image 20230719152007.png",
|
||||||
|
"GET.md",
|
||||||
"Pasted image 20230719102301.png",
|
"Pasted image 20230719102301.png",
|
||||||
"assets/Pasted image 20230719102322.png",
|
"assets/Pasted image 20230719102322.png",
|
||||||
"assets",
|
"assets",
|
||||||
|
164
Database.md
Normal file
164
Database.md
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# Spring Data and a database dependencies
|
||||||
|
|
||||||
|
This project was originally created using the [Spring Initializr](https://start.spring.io/), which allowed us to automatically add dependencies to our project. However, now we must manually add dependencies to our project.
|
||||||
|
|
||||||
|
In `build.gradle` file
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
dependencies {
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
|
|
||||||
|
// Add the two dependencies below
|
||||||
|
implementation 'org.springframework.data:spring-data-jdbc'
|
||||||
|
testImplementation 'com.h2database:h2'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The two dependencies we added are related, but different.
|
||||||
|
|
||||||
|
## Spring Data JDBC
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
implementation 'org.springframework.data:spring-data-jdbc'
|
||||||
|
```
|
||||||
|
|
||||||
|
Spring Data - has many implementations for a variety of relational and non-relational database technologies. Spring Data also has several abstractions on top of those technologies. These are commonly called an Object-Relational Mapping framework, or ORM.
|
||||||
|
|
||||||
|
Here we'll elect to use [Spring Data JDBC](https://spring.io/projects/spring-data-jdbc).
|
||||||
|
|
||||||
|
> Spring Data JDBC aims at being conceptually easy...This makes Spring Data JDBC a simple, limited, opinionated ORM.
|
||||||
|
|
||||||
|
## The actual database
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
testImplementation 'com.h2database:h2'
|
||||||
|
```
|
||||||
|
|
||||||
|
Database management frameworks only work if they have a linked database. H2 is a "very fast, open source, JDBC API" SQL database implemented in Java. It works seamlessly with Spring Data JDBC.
|
||||||
|
|
||||||
|
- Note `testImplementation`
|
||||||
|
This tells Spring Boot to make the H2 database available only when running tests. Eventually we'll need a database outside of a testing context, but not yet.
|
||||||
|
|
||||||
|
# Create the `CashCardRepository`
|
||||||
|
|
||||||
|
Create `src/main/java/djmil/cashcard/CashCardRepository.java` and have it `extend CrudRepository`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package djmil.cashcard;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
|
||||||
|
public interface CashCardRepository extends CrudRepository<CashCard, Long> {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We have to indicated which data object the `CashCardRepository` should manage. For our application, the "domain type" of this repository will be the `CashCard`. When we configure the repository as `CrudRepository<CashCard, Long>` we indicate that the `CashCard`'s ID is `Long`. However, we still need to tell Spring Data which field is the ID.
|
||||||
|
|
||||||
|
Edit the `CashCard` class to configure the `id` as the `@Id` for the `CashCardRepository`.
|
||||||
|
|
||||||
|
Don't forget to add the new import.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package djmil.cashcard;
|
||||||
|
|
||||||
|
// Add this import
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
|
||||||
|
public record CashCard(@Id Long id, Double amount) {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Inject the `CashCardRepository` into `CashCardController`
|
||||||
|
|
||||||
|
Although we've configured our `CashCard` and `CashCardRepository` classes, we haven't utilized the new `CashCardRepository` to manage our `CashCard` data. Let's do that now.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/cashcards")
|
||||||
|
public class CashCardController {
|
||||||
|
private CashCardRepository cashCardRepository;
|
||||||
|
|
||||||
|
public CashCardController(CashCardRepository cashCardRepository) {
|
||||||
|
this.cashCardRepository = cashCardRepository;
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
The tests will now pass, despite no other changes to the codebase utilizing the new, _required_ constructor `CashCardController(CashCardRepository cashCardRepository)`!
|
||||||
|
|
||||||
|
Spring's Auto Configuration is utilizing its dependency injection (DI) framework, specifically _constructor injection_, to supply `CashCardController` with the correct implementation of `CashCardRepository` at runtime.
|
||||||
|
|
||||||
|
## Learn to read compiler error
|
||||||
|
|
||||||
|
Temporarily change the `CashCardRepository` to remove the implementation of `CrudRepository`
|
||||||
|
```java
|
||||||
|
public interface CashCardRepository {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Compile the project and note the failure.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
[~/exercises] $ ./gradlew build
|
||||||
|
|
||||||
|
## shouldNotReturnACashCardWithAnUnknownId()
|
||||||
|
java.lang.IllegalStateException: Failed to load ApplicationContext for
|
||||||
|
...
|
||||||
|
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'cashCardController' defined in file [/Users/oxbee/code/hqlxa/FamilyCashCard/build/classes/java/main/djmil/cashcard/CashCardController.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'djmil.cashcard.CashCardRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
|
||||||
|
...
|
||||||
|
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'djmil.cashcard.CashCardRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Clues such as `NoSuchBeanDefinitionException`, `No qualifying bean`, and `expected at least 1 bean which qualifies as autowire candidate` tell us that Spring is trying to find a properly configured class to provide during the dependency injection phase of Auto Configuration, but none qualify. We can satisfy this DI requirement by implementing the `CrudRepository`.
|
||||||
|
|
||||||
|
# Use the `CashCardRepository` for data management
|
||||||
|
|
||||||
|
The `CrudRepository` interface provides [many helpful methods](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/CrudRepository.html#findById-ID-), including `findById(ID id)`. Update the `CashCardController` to utilize this method on the `CashCardRepository` and update the logic; be sure to `import java.util.Optional;`
|
||||||
|
|
||||||
|
```java
|
||||||
|
import java.util.Optional;
|
||||||
|
...
|
||||||
|
|
||||||
|
@GetMapping("/{requestedId}")
|
||||||
|
public ResponseEntity<CashCard> findById(@PathVariable Long requestedId) {
|
||||||
|
Optional<CashCard> cashCardOptional = cashCardRepository.findById(requestedId);
|
||||||
|
|
||||||
|
if (cashCardOptional.isPresent()) {
|
||||||
|
return ResponseEntity.ok(cashCardOptional.get());
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We're calling `CrudRepository.findById` which returns an `Optional`. This smart object _might or might not_ contain the `CashCard` for which we're searching. Learn more about `Optional` [here](https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html).
|
||||||
|
|
||||||
|
# Configure an in-memory Database
|
||||||
|
|
||||||
|
Spring Data and H2 can automatically create and populate the in-memory database we need for our test. The tests would expect the API to find and return a `CashCard` with `id` of `99`. If you'd to run test not - you'd get a `500 INTERNAL_SERVER_ERROR`, meaning that our application has no database to connect to. So let's create one!
|
||||||
|
|
||||||
|
## schema.sql - create database/tables
|
||||||
|
|
||||||
|
Spring Data will automatically configure a database for tests if we provide `src/test/resources/schema.sql`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE cash_card
|
||||||
|
(
|
||||||
|
ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
AMOUNT NUMBER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
A database schema is a "blueprint" for how data is stored in a database. Our database schema reflects the `CashCard` object that we understand, which contains an `id` and an `amount`.
|
||||||
|
|
||||||
|
Running tests at this moment will produce `404 NOT_FOUND`, meaning that we have to populate freshly created DB with some test data.
|
||||||
|
|
||||||
|
## data.sql - load data into DB
|
||||||
|
|
||||||
|
`src/test/resources/data.sql`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO CASH_CARD(ID, AMOUNT) VALUES (99, 123.45);
|
||||||
|
```
|
||||||
|
|
239
Home.md
239
Home.md
@ -1,97 +1,142 @@
|
|||||||
This repo is my first attempt to learn `SpringBoot` following [this](https://spring.academy/courses/building-a-rest-api-with-spring-boot/lessons/introduction) tutorial. The setup is Visual Code IDE alongside with [SpringBoot](https://code.visualstudio.com/docs/java/java-spring-boot) plugin.
|
This repo is my first attempt to learn `SpringBoot` following [this](https://spring.academy/courses/building-a-rest-api-with-spring-boot/lessons/introduction) tutorial. The setup is Visual Code IDE alongside with [SpringBoot](https://code.visualstudio.com/docs/java/java-spring-boot) plugin.
|
||||||
|
|
||||||
# Spring Initializr
|
# Spring Initializr
|
||||||
|
|
||||||
In VsCode press `cmd+shif+p` and type `Spring Initilizr`. Choose:
|
In VsCode press `cmd+shif+p` and type `Spring Initilizr`. Choose:
|
||||||
- Gradle Project
|
- Gradle Project
|
||||||
- _SpringBoot version:_ latest (3.1.1)
|
- _SpringBoot version:_ latest (3.1.1)
|
||||||
- _project language:_ Java
|
- _project language:_ Java
|
||||||
- _group id:_ djmil
|
- _group id:_ djmil
|
||||||
- _artifact id (aka project name):_ cashcard
|
- _artifact id (aka project name):_ cashcard
|
||||||
- _packaging type:_ jar
|
- _packaging type:_ jar
|
||||||
- _java version:_ 17
|
- _java version:_ 17
|
||||||
- _dependencies:_
|
- _dependencies:_
|
||||||
- SpringWeb
|
- SpringWeb
|
||||||
|
|
||||||
Essentially, this will generate a default minimalistic jet functional SpringBoot project. Entry point, aka `main()` can be found in [src/main/java/djmil/cashcard/CashcardApplication.java](http://192.168.8.55:3000/HQLAx/FamilyCashCard/src/branch/main/src/main/java/djmil/cashcard/CashcardApplication.java). To run the application - press `ctrl+F5` or Play button in the top right corner of an editor.
|
Essentially, this will generate a default minimalistic jet functional SpringBoot project. Entry point, aka `main()` can be found in [src/main/java/djmil/cashcard/CashcardApplication.java](http://192.168.8.55:3000/HQLAx/FamilyCashCard/src/branch/main/src/main/java/djmil/cashcard/CashcardApplication.java). To run the application - press `ctrl+F5` or Play button in the top right corner of an editor.
|
||||||
|
|
||||||
# TDD
|
# TDD
|
||||||
|
|
||||||
Different tests can be written at different levels of the system. At each level, there is a balance between the speed of execution, the “cost” to maintain the test, and the confidence it brings to system correctness. This hierarchy is often represented as a “testing pyramid”.
|
Software development teams love to move fast. So how do you go fast forever? By continuously improving and simplifying your code – this is called **refactoring**. Refactoring is the act of altering the implementation of a software system without altering its inputs, outputs, or behavior. One of the only ways you can safely refactor is when you have a trustworthy test suite. Which brings us to the **TDD Cycle**:
|
||||||
|
|
||||||
![Testing pyramid](https://blog.missiondata.com/wp-content/uploads/MD_TestingPyramid2x-1560x1045.png "Testing pyramid")
|
1. **Red:** Write a failing test for the desired functionality.
|
||||||
|
2. **Green:** Implement the simplest thing that can work to make the test pass.
|
||||||
## Unit Tests
|
3. **Refactor:** Look for opportunities to simplify, reduce duplication, or otherwise improve the code without changing any behavior - to _refactor._
|
||||||
[[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.
|
4. Repeat!
|
||||||
|
|
||||||
## Integration Tests
|
Different tests can be written at different levels of the system. At each level, there is a balance between the speed of execution, the “cost” to maintain the test, and the confidence it brings to system correctness. This hierarchy is often represented as a “testing pyramid”.
|
||||||
[[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.
|
|
||||||
|
![Testing pyramid](https://blog.missiondata.com/wp-content/uploads/MD_TestingPyramid2x-1560x1045.png "Testing pyramid")
|
||||||
## 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.
|
## Unit Tests
|
||||||
|
|
||||||
## TDD Cycle
|
[[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.
|
||||||
|
|
||||||
Software development teams love to move fast. So how do you go fast forever? By continuously improving and simplifying your code – this is called **refactoring**. One of the only ways you can safely refactor is when you have a trustworthy test suite. Thus, the best time to refactor the code you're currently focusing on is during the TDD cycle. This is called the Red, Green, Refactor development loop:
|
## Integration Tests
|
||||||
|
|
||||||
1. **Red:** Write a failing test for the desired functionality.
|
[[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.
|
||||||
2. **Green:** Implement the simplest thing that can work to make the test pass.
|
|
||||||
3. **Refactor:** Look for opportunities to simplify, reduce duplication, or otherwise improve the code without changing any behavior—to _refactor._
|
## End-to-End Tests
|
||||||
4. Repeat!
|
|
||||||
|
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.
|
||||||
# RESTful API
|
|
||||||
|
# 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.
|
|
||||||
|
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|
|
|
||||||
|---|---|---|---|
|
|Operation|API Endpoint|HTTP Method|Response Status|
|
||||||
|**C**reate|`/cashcards`|`POST`|201 (CREATED)|
|
|---|---|---|---|
|
||||||
|**R**ead|`/cashcards/{id}`|`GET`|200 (OK)|
|
|**C**reate|`/cashcards`|`POST`|201 (CREATED)|
|
||||||
|**U**pdate|`/cashcards/{id}`|`PUT`|204 (NO DATA)|
|
|**R**ead|`/cashcards/{id}`|`GET`|200 (OK)|
|
||||||
|**D**elete|`/cashcards/{id}`|`DELETE`|204 (NO DATA)|
|
|**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.
|
|
||||||
|
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
|
|
||||||
|
## 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.
|
|
||||||
|
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.
|
|
||||||
|
> We will annotate a class with a `@RestController` 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]]
|
|
||||||
|
![[Pasted image 20230719102322.png]]
|
||||||
## @RestController
|
|
||||||
|
## @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`
|
|
||||||
|
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
|
```java
|
||||||
public class CashCardController {
|
@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.
|
|
||||||
|
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
|
|
||||||
****
|
## Get
|
||||||
In [[GET]] requests, the body is empty. So, the request to read the Cash Card with an id of 123 would be:
|
****
|
||||||
|
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
|
Request:
|
||||||
URL: http://cashcard.example.com/cashcards/123
|
Method: GET
|
||||||
Body: (empty)
|
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:
|
|
||||||
|
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
|
Response:
|
||||||
Body:
|
Status Code: 200
|
||||||
{
|
Body:
|
||||||
"id": 123,
|
{
|
||||||
"amount": 25.00
|
"id": 123,
|
||||||
}
|
"amount": 25.00
|
||||||
```
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Database
|
||||||
|
|
||||||
|
The [**Separation of Concerns**](https://en.wikipedia.org/wiki/Separation_of_concerns) principle states that well-designed software should be modular, with each module having distinct and separate concerns from any other module.
|
||||||
|
|
||||||
|
Up until now, our codebase only returns a hard-coded response from the Controller. This setup violates the Separation of Concerns principle by mixing the concerns of a Controller, which is an abstraction of a web interface, with the concerns of reading and writing data to a data store, such as a database. In order to solve this, we’ll use a common software architecture pattern to enforce data management separation via the **[Repository](https://www.baeldung.com/java-dao-vs-repository)** pattern.
|
||||||
|
|
||||||
|
A common architectural framework that divides these layers, typically by function or value, such as business, data, and presentation layers, is called **Layered Architecture**. Here we can think of our Repository and Controller as two layers in a Layered Architecture. The Controller is in a layer near the Client (as it receives and responds to web requests) while the Repository is in a layer near the data store (as it reads from and writes to the data store). There may be intermediate layers as well, as dictated by business needs.
|
||||||
|
|
||||||
|
![[Pasted image 20230719152007.png]]
|
||||||
|
|
||||||
|
The Repository is the interface between the application and the database, and provides a **common abstraction** for any database, making it easier to switch to a different database when needed.
|
||||||
|
|
||||||
|
[Spring Data](https://spring.io/projects/spring-data) works with Spring Boot to make database integration simple.
|
||||||
|
|
||||||
|
## Choosing a Database
|
||||||
|
|
||||||
|
For our database selection, we’ll use an **embedded, in-memory** database. “Embedded” simply means that it’s a Java library, so it can be added to the project just like any other dependency. “In-memory” means that it stores data in memory only, as opposed to persisting data permanent, durable storage.
|
||||||
|
|
||||||
|
The specific in-memory database we’ll use is [H2](https://www.h2database.com/html/main.html). Fortunately, H2 is highly compatible with other relational databases, so dev-prod parity *(application might behave differently when running the in-memory database than when running in production)* won’t be a big issue. We’ll use H2 for **convenience for local development**, but want to recognize the tradeoffs.
|
||||||
|
|
||||||
|
## Auto Configuration
|
||||||
|
|
||||||
|
Simply by adding [[Database#Spring Data and a database dependencies]] we are getting full database functionality. This wonderfully showcases one of the most powerful features of Spring Boot: **Auto Configuration**. Without Spring Boot, we’d have to configure Spring Data to speak to H2. However, because we’ve included the Spring Data dependency (and a specific data provider, H2), Spring Boot will automatically configure your application to communicate with H2.
|
||||||
|
|
||||||
|
## Spring Data’s CrudRepository
|
||||||
|
|
||||||
|
For our Repository selection, we’ll use a specific type of Repository: Spring Data’s `CrudRepository`. At first glance, it’s slightly magical, but let’s unpack that magic.
|
||||||
|
|
||||||
|
The following is a complete implementation of all CRUD operations by extending `CrudRepository`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface CashCardRepository extends CrudRepository<CashCard, Long> {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With just the above code, a caller can call any number of predefined `CrudRepository` methods, such as `findById`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
cashCard = cashCardRepository.findById(99);
|
||||||
|
```
|
||||||
|
|
||||||
|
You might immediately wonder: where is the implementation of the `CashCardRepository.findById()` method? `CrudRepository` and everything it inherits from is an Interface with no actual code! Well, based on the specific Spring Data framework used, which for us will be Spring Data JDBC, Spring Data takes care of this implementation for us during the IoC container startup time. The Spring runtime will then expose the repository as yet another bean that you can reference wherever needed in your application.
|
||||||
|
|
||||||
|
As we’ve learned, there are typically trade-offs. For example the `CrudRepository` generates SQL statements to read and write your data, which is useful for many cases, but sometimes you need to write your own custom SQL statements for specific use cases. But for now, we’re happy to take advantage of its convenient, out-of-the-box methods,
|
@ -93,4 +93,29 @@ Number id = documentContext.read("$.id");
|
|||||||
assertThat(id).isNotNull();
|
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`.
|
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`.
|
||||||
|
|
||||||
|
# Complex test debugging
|
||||||
|
|
||||||
|
To make tests output more verbose, add this section to the `build.gradle` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
// This section causes useful test output to go to the terminal.
|
||||||
|
test {
|
||||||
|
testLogging {
|
||||||
|
events "passed", "skipped", "failed" //, "standardOut", "standardError"
|
||||||
|
|
||||||
|
showExceptions true
|
||||||
|
exceptionFormat "full"
|
||||||
|
showCauses true
|
||||||
|
showStackTraces true
|
||||||
|
|
||||||
|
// Change to `true` for more verbose test output
|
||||||
|
showStandardStreams = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Additional resources
|
||||||
|
|
||||||
|
- [Integration Testing in Spring](https://www.baeldung.com/integration-testing-in-spring) written by baeldung.
|
230
UnitTests.md
230
UnitTests.md
@ -1,115 +1,115 @@
|
|||||||
|
|
||||||
# My first unit test
|
# My first unit test
|
||||||
|
|
||||||
Let's start with the simplest thing you can imagine: a single test method with a single statement. Create [src/test/java/example/cashcard/CashCardJsonTest.java](http://192.168.8.55:3000/HQLAx/FamilyCashCard/src/commit/5ff71154302523ab5ebd0a291e3f5819aed8fdb9/src/test/java/djmil/cashcard/CashCardJsonTest.java):
|
Let's start with the simplest thing you can imagine: a single test method with a single statement. Create [src/test/java/example/cashcard/CashCardJsonTest.java](http://192.168.8.55:3000/HQLAx/FamilyCashCard/src/commit/5ff71154302523ab5ebd0a291e3f5819aed8fdb9/src/test/java/djmil/cashcard/CashCardJsonTest.java):
|
||||||
|
|
||||||
``` java
|
``` java
|
||||||
package djmil.cashcard;
|
package djmil.cashcard;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
public class CashCardJsonTest {
|
public class CashCardJsonTest {
|
||||||
@Test
|
@Test
|
||||||
public void myFirstTest() {
|
public void myFirstTest() {
|
||||||
assertThat(1).isEqualTo(42);
|
assertThat(1).isEqualTo(42);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `@Test` annotation is part of the JUnit library, and the `assertThat` method is part of the AssertJ library. Both of these libraries are imported after the package statement.
|
The `@Test` annotation is part of the JUnit library, and the `assertThat` method is part of the AssertJ library. Both of these libraries are imported after the package statement.
|
||||||
|
|
||||||
A common convention (but not a requirement) is to always use the Test suffix for test classes. We’ve done that here. The full class name CashCardJsonTest.java gives you a clue about the nature of the test we're about to write.
|
A common convention (but not a requirement) is to always use the Test suffix for test classes. We’ve done that here. The full class name CashCardJsonTest.java gives you a clue about the nature of the test we're about to write.
|
||||||
|
|
||||||
In true Test-First fashion, we've written a failing test first. It's important to have a failing test first so you can have high confidence that whatever you did to fix the test actually worked.
|
In true Test-First fashion, we've written a failing test first. It's important to have a failing test first so you can have high confidence that whatever you did to fix the test actually worked.
|
||||||
|
|
||||||
Toggle terminal with `ctrl+tilda` and type
|
Toggle terminal with `ctrl+tilda` and type
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew test
|
./gradlew test
|
||||||
```
|
```
|
||||||
|
|
||||||
# Testing the CashCard Data Contract
|
# Testing the CashCard Data Contract
|
||||||
|
|
||||||
```java
|
```java
|
||||||
import org.springframework.boot.test.json.JacksonTester;
|
import org.springframework.boot.test.json.JacksonTester;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
@JsonTest
|
@JsonTest
|
||||||
public class CashCardJsonTest {
|
public class CashCardJsonTest {
|
||||||
@Autowired
|
@Autowired
|
||||||
private JacksonTester<CashCard> json;
|
private JacksonTester<CashCard> json;
|
||||||
```
|
```
|
||||||
|
|
||||||
Marking CashCardJsonTest with `@JsonTest` annotation makes it a test class which uses the Jackson framework (which is included as part of Spring). This provides extensive JSON testing and parsing support. It also establishes all the related behavior to test JSON objects.
|
Marking CashCardJsonTest with `@JsonTest` annotation makes it a test class which uses the Jackson framework (which is included as part of Spring). This provides extensive JSON testing and parsing support. It also establishes all the related behavior to test JSON objects.
|
||||||
|
|
||||||
### @Autowired
|
### @Autowired
|
||||||
|
|
||||||
`@Autowired` is an annotation that directs Spring to create an object of the requested type. `JacksonTester` is a convenience wrapper to the Jackson JSON parsing library. It handles serialization and deserialization of JSON objects.
|
`@Autowired` is an annotation that directs Spring to create an object of the requested type. `JacksonTester` is a convenience wrapper to the Jackson JSON parsing library. It handles serialization and deserialization of JSON objects.
|
||||||
|
|
||||||
To create a CashCard class and the constructor that’s used in the `cashCardSerializationTest()` test, create the file `src/main/java/djmil/cashcard/CashCard.java` with the following contents (notice that this file is under in the `src/main` directory, not the `src/test` directory):
|
To create a CashCard class and the constructor that’s used in the `cashCardSerializationTest()` test, create the file `src/main/java/djmil/cashcard/CashCard.java` with the following contents (notice that this file is under in the `src/main` directory, not the `src/test` directory):
|
||||||
|
|
||||||
```java
|
```java
|
||||||
package djmil.cashcard;
|
package djmil.cashcard;
|
||||||
|
|
||||||
public record CashCard(Long id, Double amount) {
|
public record CashCard(Long id, Double amount) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### The contract file
|
### The contract file
|
||||||
|
|
||||||
`src/test/resources/djmil/cashcard/expected.json`
|
`src/test/resources/djmil/cashcard/expected.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": 99,
|
"id": 99,
|
||||||
"amount": 123.45
|
"amount": 123.45
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**NOTE** Resources
|
**NOTE** Resources
|
||||||
Pay attention to the path `djmil/cashcard/` is essentially a *package name*. It is shared between different aspects of the project:
|
Pay attention to the path `djmil/cashcard/` is essentially a *package name*. It is shared between different aspects of the project:
|
||||||
- src/main/java - code
|
- src/main/java - code
|
||||||
- src/tests/java - tests
|
- src/tests/java - tests
|
||||||
- src/tests/resources - static resources for testing.
|
- src/tests/resources - static resources for testing.
|
||||||
Essentially `gradle` is responsible to map different parts of source code onto final package to be accessible for java via *classpath*.
|
Essentially `gradle` is responsible to map different parts of source code onto final package to be accessible for java via *classpath*.
|
||||||
|
|
||||||
### The test
|
### The test
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@Test
|
@Test
|
||||||
public void cashCardSerializationTest() throws IOException {
|
public void cashCardSerializationTest() throws IOException {
|
||||||
CashCard cashCard = new CashCard(99L, 123.45);
|
CashCard cashCard = new CashCard(99L, 123.45);
|
||||||
|
|
||||||
assertThat(json.write(cashCard)).isStrictlyEqualToJson("expected.json");
|
assertThat(json.write(cashCard)).isStrictlyEqualToJson("expected.json");
|
||||||
|
|
||||||
assertThat(json.write(cashCard)).hasJsonPathNumberValue("@.id");
|
assertThat(json.write(cashCard)).hasJsonPathNumberValue("@.id");
|
||||||
assertThat(json.write(cashCard)).extractingJsonPathNumberValue("@.id")
|
assertThat(json.write(cashCard)).extractingJsonPathNumberValue("@.id")
|
||||||
.isEqualTo(99);
|
.isEqualTo(99);
|
||||||
|
|
||||||
assertThat(json.write(cashCard)).hasJsonPathNumberValue("@.amount")
|
assertThat(json.write(cashCard)).hasJsonPathNumberValue("@.amount")
|
||||||
assertThat(json.write(cashCard)).extractingJsonPathNumberValue("@.amount")
|
assertThat(json.write(cashCard)).extractingJsonPathNumberValue("@.amount")
|
||||||
.isEqualTo(123.45);
|
.isEqualTo(123.45);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`.isStrictlyEqualToJson("expected.json");` will try to load static file from `FamilyCashCard/build/resources/test/djmil/cashcard` directory.
|
`.isStrictlyEqualToJson("expected.json");` will try to load static file from `FamilyCashCard/build/resources/test/djmil/cashcard` directory.
|
||||||
|
|
||||||
# Testing Deserialization
|
# Testing Deserialization
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@Test
|
@Test
|
||||||
public void cashCardDeserializationTest() throws IOException {
|
public void cashCardDeserializationTest() throws IOException {
|
||||||
String expected = """
|
String expected = """
|
||||||
{
|
{
|
||||||
"id":1000,
|
"id":1000,
|
||||||
"amount":67.89
|
"amount":67.89
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
assertThat(json.parse(expected)).isEqualTo(new CashCard(1000L, 67.89));
|
assertThat(json.parse(expected)).isEqualTo(new CashCard(1000L, 67.89));
|
||||||
assertThat(json.parseObject(expected).id()).isEqualTo(1000);
|
assertThat(json.parseObject(expected).id()).isEqualTo(1000);
|
||||||
assertThat(json.parseObject(expected).amount()).isEqualTo(67.89);
|
assertThat(json.parseObject(expected).amount()).isEqualTo(67.89);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
BIN
assets/Pasted image 20230719152007.png
Normal file
BIN
assets/Pasted image 20230719152007.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 141 KiB |
Loading…
Reference in New Issue
Block a user