database

djmil 2023-07-20 12:33:27 +02:00
parent 997c67f20a
commit d35729d123
6 changed files with 459 additions and 223 deletions

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

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

@ -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 theyre 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 theyre 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 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.
> We will annotate a class with a `@RestController` 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]]
![[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 {
``` }
```
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.
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
**** ## 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, well 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, well use an **embedded, in-memory** database. “Embedded” simply means that its 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 well 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)* wont be a big issue. Well 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, wed have to configure Spring Data to speak to H2. However, because weve included the Spring Data dependency (and a specific data provider, H2), Spring Boot will automatically configure your application to communicate with H2.
## Spring Datas CrudRepository
For our Repository selection, well use a specific type of Repository: Spring Datas `CrudRepository`. At first glance, its slightly magical, but lets 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 weve 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, were 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.

@ -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. Weve 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. Weve 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 thats 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 thats 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);
} }
``` ```

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB