In diesem Artikel erfahren Sie, wie Sie Ihre erste API mit Spring Boot und Kotlin erstellen.
Spring Framework
Spring Framework 🍃, das beliebteste Framework in Java für die Anwendungsentwicklung. Millionen von Entwicklern nutzen Spring täglich, um alle Arten von Anwendungen zu erstellen. Die erste Veröffentlichung von Spring im Jahr 2003 konzentrierte sich auf Dependency Injection, aber das Framework ist enorm gewachsen. Das Spring-Team hat eine Reihe von Modulen erstellt, die fast alles unterstützen, was ein Entwickler zum Erstellen einer Anwendung benötigt. Angesichts dieser Reife ist es kein Wunder, dass das Framework in den weltweit führenden Technologieunternehmen eingesetzt wird. Das ist der Vorteil eines Projekts, das seit fast 20 Jahren ständig weiterentwickelt und verbessert wird.
Was ist Kotlin
Kotlin ist eine Sprache, die von Jetbrains entwickelt wurde, derselben Firma, die alle großartigen IDEs wie IntelliJ Idea erstellt hat. JetBrains war mit Java und den anderen Sprachen auf dem Markt nicht zufrieden und beschloss, Kotlin zu entwickeln, um eine Verbesserung gegenüber Java zu erzielen. Und wenn ich von Verbesserung spreche, dann meine ich damit, dass Kotlin vollständig kompatibel mit Java gestaltet wurde, was es ermöglicht, eine gemeinsame Codebasis zwischen diesen beiden Sprachen zu haben und Unternehmen eine schrittweise Migration von Java zu ermöglichen.
Kotlin wird bei Entwicklern immer beliebter und laut der StackOverflow-Umfrage 2020 ist Kotlin eine der am meisten geliebten Sprachen. All diese Liebe kommt nicht von ungefähr. Kotlin hat viele Funktionen, die die Entwicklung einfacher, weniger wortreich und fehlerresistenter machen. Eine der besten Eigenschaften ist ein natives Null-sicheres System, Erweiterungsklassen, höherwertige Funktionen und die async await-Version der Sprache, die Coroutines genannt wird.
Erstellung des Projekts
Um mit der Erstellung unseres Projekts zu beginnen, werden wir das Spring Initializer-Tool verwenden. Sie finden es unter start.spring.io. Dies ist ein Tool, das vom Spring-Team erstellt wurde und sehr nützlich ist, da wir bereits unser initiales Setup durchführen und unsere Hauptabhängigkeiten einbinden können.
Zuerst können Sie den gesamten Quellcode hier auf meinem Github finden. Für dieses Projekt werden wir ein Setup verwenden, das sich ein wenig von den anderen häufigen Tutorials unterscheidet, die wir mit Spring Boot finden können. Wir werden Gradle als unser Build-Tool verwenden, aber natürlich, wie Sie sehen können, ist auch Maven eine Option. Wir werden die stabilste Version von Spring wählen, das ist 2.6.0 (als ich diesen Artikel schreibe). Sie können die Projekt-Metadaten ändern, wenn Sie möchten, oder einfach die Standardwerte belassen. Ich habe Java 11 gewählt, da ich Java 17 noch nicht installiert habe, aber für dieses Projekt wird es keinen Unterschied machen.
In der rechten Spalte haben wir die Option, unsere initialen Abhängigkeiten einzubinden. Für dieses Projekt werden wir einige von ihnen einbeziehen und später weitere Abhängigkeiten manuell in unser Gradle-File hinzufügen. Lassen Sie uns beginnen mit:
- Spring Boot DevTools — Es gibt einige nützliche Funktionen, die uns während der Entwicklung helfen können, wie automatisches Neuladen, Live-Reload und Deaktivierung des Cachings. Sehr nützlich während der Entwicklung.
- Spring Web — Wir werden benötigen, um unsere API’s zu erstellen
- Spring JPA und H2 Database — zum Speichern unserer Daten
Wie Sie in meinen Metadaten sehen können, werde ich ein Beispiel erstellen, das eine Bank simuliert, und wir werden eine Transfer-API erstellen. 💰
IntelliJ Idea
Entpacken Sie die Datei und öffnen Sie sie in Ihrer IDE. Ich werde IntelliJ Idea Community Edition verwenden. Sie können es kostenlos herunterladen oder eine andere IDE verwenden. Erwähnenswert ist, dass, wenn Sie die Ultimate-Version von IntelliJ Idea haben, das Spring Initializer Tool, das wir gerade gesehen haben, bereits in die IDE eingebettet ist, sodass Sie die gesamte Projekt-Erstellung direkt dort durchführen können.
Gradle-Datei
Das erste, was wir uns anschauen werden, ist unsere build.gradle.kts
-Datei. Sie befindet sich im Stammverzeichnis des Projektordners. Dies ist die Datei, in der wir unsere Abhängigkeiten verwalten können. Da wir den Spring Initializer für die anfängliche Einrichtung verwendet haben, können wir sehen, dass das Tool diese Datei mit unserer Konfiguration und den ausgewählten Abhängigkeiten erstellt hat.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.5.7"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.5.31"
kotlin("plugin.spring") version "1.5.31"
kotlin("plugin.jpa") version "1.5.31"
}
group = "com.backendhorizon"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
Da wir uns entschieden haben, Kotlin zu verwenden, hat Spring Initializer diese Konfigurationsdatei bereits als Kotlin-Datei erstellt. Für diejenigen, die mit Gradle nicht vertraut sind, weise ich darauf hin, dass es auch möglich ist, Ihre Gradle-Dateien mit Groovy zu schreiben. Da wir bereits Kotlin verwenden, empfehle ich dringend, es auch mit Gradle zu verwenden, da es einige Vorteile gegenüber Groovy bietet, wie zum Beispiel die Autovervollständigung.
Gradle-Plugins
Lassen Sie uns den Plugins-Block ansehen. Gradle selbst bietet standardmäßig nicht viel, aber es bietet uns Entwicklern die Möglichkeit, unsere Builds zu erweitern und neue Funktionen hinzuzufügen. Ein großartiges Beispiel hierfür ist der „plugins“-Block, in dem wir Plugins haben, die unser Projekt sehr einfach einsatzbereit machen.
Wir sehen, dass wir einige Plugins haben und die meisten von ihnen für eine Ersteinrichtung von Spring und Kotlin vorgesehen sind. Werfen wir einen Blick auf die Spring Boot Plugin-Seite und sehen, wie das Plugin beschrieben wird:
Spring Boot macht es einfach, eigenständige, produktionsreife, Spring-basierte Anwendungen zu erstellen, die Sie „einfach ausführen“ können. Wir nehmen eine meinungsstarke Sicht auf die Spring-Plattform und Drittanbieterbibliotheken ein, sodass Sie mit minimalem Aufwand loslegen können. Die meisten Spring Boot-Anwendungen benötigen sehr wenig Spring-Konfiguration.
Mit anderen Worten, einfache Konfiguration, um sofort mit Ihrem Projekt zu beginnen. Alle anderen Plugins tun etwas Ähnliches: Sie bereiten unsere Projektabhängigkeiten vor und helfen, sie auf die bestmögliche Weise zu konfigurieren und zu integrieren.
Unter den Plugins haben wir die Metadaten, die wir wählen, und die Java-Version.
Maven Central ist das Repository, aus dem wir unsere Abhängigkeiten herunterladen, und im Block dependencies
sind alle Bibliotheken aufgeführt, die wir im Projekt haben. Später werden wir zu dieser Datei zurückkehren und einige zusätzliche Abhängigkeiten hinzufügen.
Es gibt eine weitere wichtige Datei, die einen kurzen Blick wert ist, und das ist unsere Anwendungsdatei.
Application-Datei
Lassen Sie uns unsere Application-Datei öffnen. Im Projekt auf GitHub können Sie diese Datei als BankApplication
finden, aber dies wird sich je nach Wahl in Ihren Metadaten ändern. Hier finden wir die berühmteste Funktion von allen, die main-Funktion, wo die Anwendung startet.
package com.backendhorizon.bank
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class BankApplication
fun main(args: Array<String>) {
runApplication<BankApplication>(*args)
}
Wir können sehen, dass in der main-Methode diese Funktion runApplication
verwendet wird, um mit der BankApplication
-Klasse zu starten, und in dieser Klasse haben wir nur eine einzige Annotation, @SpringBootApplication
. Diese einzelne Annotation übernimmt eine Menge für uns.
Werfen wir einen kurzen Blick darauf, wie die Dokumentation dies beschreibt:
Gibt eine Konfigurationsklasse an, die eine oder mehrere
@Bean
-Methoden deklariert und auch die automatische Konfiguration und das Scannen von Komponenten auslöst. Dies ist eine Komfort-Anmerkung, die der Deklaration von@Configuration
,@EnableAutoConfiguration
und@ComponentScan
entspricht."
Im nächsten Kapitel werden wir unseren ersten Controller erstellen und etwas, das Sie bemerken werden, ist, dass wir keine Instanz des Controllers erstellen werden. Das liegt daran, dass die @SpringBootApplication
-Annotation aus 3 anderen Annotationen besteht und eine davon ist die @ComponentScan
.
Wie der Name bereits vermuten lässt, wird @ComponentScan
unser Projekt nach Components durchsuchen/scannen und wenn sie gefunden wird, wird Spring diese Instanz für uns erstellen und verwalten. Es ist erwähnenswert, dass eine Komponente, damit sie automatisch erkannt wird, sich auf derselben Paketebene oder darunter der Anwendungsklasse befinden muss.
Components sind Klassen, die mit speziellen Annotationen markiert sind, welche diese Klassen zur automatischen Erkennung vorschlagen. Es gibt einige verschiedene Annotationen, die für unterschiedliche Zwecke verwendet werden können, wie z. B. @Component
, @Controller
, @RestController
, @Repository
und @Service
.
Die @EnableAutoConfiguration
wird etwas Ähnliches für die Bibliotheken im Projekt tun. Sie wird in unserem Klassenpfad nachsehen, die Bibliotheken überprüfen, die wir verwenden, und eine anfängliche Konfiguration für uns laden. Das werden wir mit unserer Datenbank sehen. Bleiben Sie dran. 😉
Erste API
Lassen Sie uns unseren ersten API-Controller erstellen. Erstellen Sie ein Paket namens controller und darin eine Klasse namens TransferController
. Erstellen Sie 2 Methoden, newTransfer
und getAllTransfers
wie im folgenden Beispiel.
package com.backendhorizon.bank.controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/transfer")
class TransferController {
@PostMapping("/new")
fun newTransfer() {
System.out.println("Saved")
}
@GetMapping("/all")
fun getAllTransfers(): String {
return "Hello"
}
}
Lass uns verstehen, was hier passiert. Am Anfang unserer TransferController
-Klasse haben wir zwei Anmerkungen verwendet, @RestController
und @RequestMapping
:
@RestController
: Mit dieser Anmerkung geben wir Spring Boot an, dass diese Klasse ein Web-Handler/Web-Controller ist. Sie wird Anfragen erhalten und Antworten an die Anrufer zurückgeben. Da es sich um eine Art von “Controller” handelt, wird diese Komponente (denken Sie daran, diesen Begriff im Hinterkopf zu behalten) auch von Spring Boot verwaltet.@RequestMapping
: Dies gibt an, welche Signatur dieser Web-Controller in der Anfrageadresse nach der Hostadresse haben wird. Zum Beispiel werden unsere beiden Methodenendpunkte die folgenden Adressen haben:localhost:8080/transfer/new
undlocalhost:8080/transfer/all
.
In Bezug auf die Methoden sind sie relativ einfach:
newTransfer
protokolliert eine “Saved”-Nachricht in der Konsole und seine Anmerkung zeigt an, dass dies eine “Post"-Anfrage sein wird,@PostMapping
, und wie der Controller gibt sie die Signatur für diese Methode an,localhost:8080/payment/new
.getAllTransfer
gibt eine “Hello”-Nachricht an den Anrufer zurück und ähnlich wie bei der vorherigen Methode gibt es den Anfragetyp an, in diesem Fall eine Get-Anfrage,@GetMapping
, gefolgt von der Signatur,localhost:8080/payment/all
.Get
undPost
sind Teil der REST-Spezifikation, falls Sie damit nicht vertraut sind, hinterlassen Sie einen Kommentar und ich werde einen neuen Artikel erstellen, der diese ausführlicher erklärt.
Ausführen & Testen
Jetzt haben wir alles, was wir für unseren ersten Test benötigen. Gehe zur main-Funktion in der Anwendungsdatei und starte deine Anwendung. Du solltest beginnen, einige Logs zu sehen und Tomcat, unser WebServer, wird starten und die Anwendung ohne Probleme hosten. Beachte in den Logs, dass Tomcat dir mitteilt, auf welchem Port deine Anwendung läuft.
Jetzt müssen wir diese Endpunkte testen. Der beste Weg ist, ein dafür entwickeltes Tool zu verwenden. Ich persönlich empfehle Postman. Es unterstützt REST-APIs vollständig und ermöglicht das Testen aller verschiedenen Arten von REST-Endpunkten, das Einrichten von JSON-Bodies, Headern, Autorisierungstoken usw. Postman hat auch GraphQL-Unterstützung, falls du darauf stehen solltest. 😂
Öffne einen neuen Tab in Postman, wähle GET und gib die Adresse des getAllTransfers
-Endpunkts ein, wie im Bild unten:
Wie du sehen kannst, haben wir eine „Hello“-Nachricht als Antwort erhalten. Unsere API funktioniert! Mache dasselbe für newTransfer
, ändere die URL und den Typ zu POST. Du wirst sehen, dass die API ebenfalls funktioniert und die „Saved“-Nachricht in der IntelliJ Idea-Konsole angezeigt wird.
Beachte, dass wir keine Instanz von TransferController erstellt haben, nur die @RestController
-Annotation hat Spring angezeigt, dass unser Controller erstellt werden muss. Läuft wie geschmiert, oder?!
Datenmodelle
Lassen Sie uns damit beginnen, unsere Anwendung vorzubereiten, um Daten zu empfangen und zu speichern. Innerhalb des Controller-Pakets erstellen Sie ein neues namens model
. Erstellen Sie zwei neue Klassen, TransactionModel
und OverviewTransactionModel
.
In der TransactionModel
Klasse werden wir ein paar Attribute erstellen. Stellen wir uns vor, dass wir für eine Überweisung an eine andere Person die Kontokennung benötigen, die je nach Land, in dem Sie leben, eine Nummer oder eine Zeichenfolge sein kann. Wir benötigen auch den Wert und eine Beschreibung, die optional sein kann.
package com.backendhorizon.bank.controller.model
class TransactionModel(
val targetAccount: String,
val value: Double,
val description: String = "",
)
Wenn wir die Liste aller Transaktionen abrufen, möchten wir ein wenig mehr Daten sehen. Neben allen bisherigen Daten haben wir zwei zusätzliche Felder, das Datum der Transaktion und die Transaktionskennung(id).
package com.backendhorizon.bank.controller.model
import java.util.*
class OverviewTransactionModel(
val targetAccount: String,
val value: Double,
val description: String,
val date: Date,
val id: String
)
Aktualisieren Sie Ihren TransferController
wie im folgenden Code mit den neuen Modellen, die wir erstellt haben. Sie können feststellen, dass wir in der newTransfer
-Methode eine neue Annotation @RequestBody
haben. Dies zeigt an, dass das TransactionModel
Teil dieser Anfrage ist und das JSON automatisch in unser Modell deserialisieren wird.
package com.backendhorizon.bank.controller
import com.backendhorizon.bank.controller.model.OverviewTransactionModel
import com.backendhorizon.bank.controller.model.TransactionModel
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/transfer")
class TransferController {
@PostMapping("/new")
fun newTransfer(@RequestBody transactionModel: TransactionModel) {
// TODO: SAVE THE DATA
}
@GetMapping("/all")
fun getAllTransfer(): List<OverviewTransactionModel> {
// TODO: FETCH THE DATA
return listOf()
}
}
Datenbank
Da wir nun Daten über unsere API empfangen, müssen wir sie in einer Datenbank speichern. Wir werden eine In-Memory-Datenbank namens H2 verwenden, die wir bereits in unserer build.gradle.kts
Datei aufgenommen haben.
Erstellen Sie ein neues Paket auf der gleichen Ebene wie controller namens repository
. Innerhalb dieses Pakets eine Schnittstelle namens TransferRepository
. Lassen Sie uns auch ein weiteres model
Paket innerhalb des Repositorys erstellen und darin eine Klasse namens TransactionDBModel
. Am Ende sollten Sie so etwas haben:
Zuerst arbeiten wir an den Attributen des DB-Modells. Ähnlich wie TransactionModel
werden wir die Kontokennung, den Wert und die Beschreibung haben. Um in die Datenbank speichern zu können, müssen wir einen eindeutigen Bezeichner haben, und dieser Bezeichner wird automatisch für uns generiert. Wir werden auch das aktuelle Datum als Transaktionsdatum verwenden.
package com.backendhorizon.bank.repository.model
import java.util.*
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
@Entity
class TransactionDBModel(
val accountIdentifier: String,
val value: Double,
val description: String = "",
) {
@Id
@GeneratedValue
var id: UUID? = null
private set
val date: Date = Date()
}
Durch den Konstruktor werden wir die Attribute empfangen, die wir über unsere API erhalten, und im Klassenkörper werden wir die zusätzlichen zwei Attribute (id und date) haben. Wir könnten die id bereits mit einer zufälligen UUID initialisieren, aber wir lassen JPA das erledigen. Wir machen das Set privat, damit wir diesen Wert nicht überschreiben, sondern nur lesen können. Das Datum wird immer das aktuelle Datum sein, also initialisieren wir es einfach mit dem aktuellen.
In dieser Klasse haben wir drei neue Anmerkungen:
@Entity
: Dies zeigt an, dass diese Klasse von JPA (Java Persistence API) verwendet wird, um die Daten in einer Datenbank zu speichern.@Id
: Zeigt an, dass das annotierte Feld für diese Entität der Primärschlüssel in der Datenbank sein wird.@GeneratedValue
: Der Name ist selbsterklärend. Es zeigt an, dass dieser Primärschlüssel seinen eigenen Wert automatisch generieren soll.
Jetzt, da wir unser Modell haben, erstellen wir unser Interface, das CRUD-Operationen (Create, Read, Update und Delete) durchführt. Die Art und Weise, wie wir dies tun werden, besteht darin, unser TransferRepository
mit CrudRepository
zu erweitern. Es ist erstaunlich, wie einfach und leistungsstark das ist.
package com.backendhorizon.bank.repository
import com.backendhorizon.bank.repository.model.TransactionDBModel
import org.springframework.data.repository.CrudRepository
import java.util.*
interface TransferRepository: CrudRepository<TransactionDBModel, UUID>
Wenn Sie das CrudRepository
-Interface öffnen, ist es leicht zu erkennen, was passiert. Dieses Interface enthält alle generischen Funktionen für CRUD und alles, was wir tun müssen, ist anzugeben, welcher Typ unser Modell und der Primärschlüssel sind, in unserem Fall TransactionDBModel
und UUID
.
Indem wir lediglich die Modell- und Primärschlüsseltypen angeben, wird Spring herausfinden, dass wir dieses Interface verwenden und eine Standardimplementierung für dieses Interface (und die von uns bereitgestellten Typen) injizieren. Sie können sich die Standardimplementierung ansehen, es handelt sich um eine Klasse namens SimpleJpaRepository
.
Natürlich sind wir nicht auf nur diese Funktionen beschränkt. Es ist auch möglich, komplexere Datenbankoperationen zu erstellen, aber für diesen Artikel arbeiten wir nur mit dieser Standardimplementierung.
Von der API zur Datenbank und umgekehrt
Jetzt, da unsere Persistenzschicht fertig ist, können wir die Daten speichern, die wir von der API erhalten. Das Einzige, was wir tun müssen, ist eine kleine Konvertierung zwischen unseren Modellen, da das Modell, das wir im Endpoint erhalten, sich von dem unterscheidet, das wir in der Datenbank verwenden. Lassen Sie uns zwei Erweiterungsfunktionen erstellen, um diese Modelle zu konvertieren, convertToDBModel
und convertToOverviewTransactionModel
.
Zuerst bei TransactionModel
, konvertieren Sie das Endpoint-Modell in ein Datenbankmodell, TransactionDBModel
.
package com.backendhorizon.bank.controller.model
import com.backendhorizon.bank.repository.model.TransactionDBModel
class TransactionModel(
val targetAccount: String,
val value: Double,
val description: String = "",
)
fun TransactionModel.convertToDBModel() = TransactionDBModel(
accountIdentifier = this.targetAccount,
value = this.value,
description = this.description
)
Und für die Übersicht, etwas Ähnliches. Diesmal jedoch das Datenbankmodell TransactionDBModel
in ein Endpoint-Modell, OverviewTransactionModel
.
package com.backendhorizon.bank.controller.model
import com.backendhorizon.bank.repository.model.TransactionDBModel
import java.util.*
class OverviewTransactionModel(
val targetAccount: String,
val value: Double,
val description: String,
val date: Date,
val id: String
)
fun TransactionDBModel.convertToOverviewTransactionModel() = OverviewTransactionModel(
targetAccount = this.accountIdentifier,
value = this.value,
description = this.description,
date = this.date,
id = this.id.toString()
)
OK, nun können wir die Daten zwischen unseren Endpunkten und der Datenbank konvertieren. Lassen Sie uns unseren Controller aktualisieren, um diese Aufrufe zu tätigen.
In den Konstruktor werden wir unsere TransferRepository
Klasse einfügen. Denken Sie daran, Spring ist schlau; wenn Spring sieht, dass wir diese Abhängigkeit in unserem Controller haben, wird Spring die für TransactionRepository
(basierend auf SimpleJpaRepository
) erstellte Implementierung injizieren.
package com.backendhorizon.bank.controller
import com.backendhorizon.bank.controller.model.TransactionModel
import com.backendhorizon.bank.controller.model.OverviewTransactionModel
import com.backendhorizon.bank.controller.model.convertToDBModel
import com.backendhorizon.bank.controller.model.convertToOverviewTransactionModel
import com.backendhorizon.bank.repository.TransferRepository
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/transfer")
class TransferController(val repository: TransferRepository) {
@PostMapping("/new")
fun newTransfer(@RequestBody transactionModel: TransactionModel) {
repository.save(transactionModel.convertToDBModel())
}
@GetMapping("/all")
fun getAllTransfers(): List<OverviewTransactionModel> {
return repository.findAll().map { it.convertToOverviewTransactionModel() }
}
}
Wie Sie im obigen Beispiel sehen können, verwendet unsere Repository-Klasse die generischen Methoden von CrudRepository
, um die Transaktionen in der Datenbank zu speichern und alle zu finden. Wir nutzen auch die Erweiterungsmethoden, die wir erstellt haben, um das Modell in das am besten geeignete umzuwandeln.
Zu diesem Zeitpunkt sind unsere Endpunkte alle fertig. Kompilieren und führen Sie die Anwendung neu aus. Lassen Sie uns eine neue Überweisungsanforderung erstellen. Denken Sie daran, dass wir jetzt alle erforderlichen Daten in Postman zu unserem Endpunkt hinzufügen müssen, damit er korrekt funktioniert. Fügen Sie einen JSON-Body wie im Bild unten ein und reichen Sie ihn ein.
Nach dem Einreichen sollte alles einwandfrei funktionieren. Sie sollten einen Status 200 erhalten, was bedeutet, dass die Anfrage erfolgreich war. Wenn Sie jetzt alle Daten erneut abrufen, sehen Sie die übermittelten Überweisungsdaten, einschließlich des Datums und der generierten ID.
Unsere API funktioniert 🎉, aber wir sind noch nicht fertig!
Database Console
Es gibt eine sehr schöne Möglichkeit, die uns die H2 Database bietet, um die gespeicherten Daten zu überprüfen, und das ist die “H2 Console”. Es ist eine Web-Admin-Oberfläche, in der Sie die Datenbankdaten überprüfen und Abfragen ausführen können.
Zuerst müssen Sie, um diese Konsole zu aktivieren, den folgenden Code in Ihre application.properties
-Datei im Ressourcen-Ordner einfügen.
# Enabling H2 Console
spring.h2.console.enabled=true
Nachdem hinzugefügt, führen Sie Ihre Anwendung erneut aus und geben Sie in Ihrem Browser die URL http://localhost:8080/h2-console
ein. Sie werden ein Anmeldefenster sehen, um sich mit Ihrer Datenbank zu verbinden. Das Einzige, was Sie ändern müssen, ist die JDBC-URL. Schauen Sie in Ihre Konsolenprotokolle und suchen Sie nach „H2 console available at …“ wie im Bild unten.
Kopieren Sie die Datenbank-URL und drücken Sie auf „Verbinden“. Sie sollten die Konsole sehen. Jetzt können Sie mit Ihrer Datenbank spielen, SQL-Abfragen ausführen und die von Ihrer API gespeicherten Daten sehen.
Jetzt sind wir fertig, oder? FALSCH! Wir sind gute Entwickler und gute Entwickler schreiben Tests!
Unit-Tests
Bevor wir unseren ersten Test schreiben, fügen wir eine neue Abhängigkeit im Abhängigkeitsblock innerhalb von build.gradle.kts
hinzu. Mit dieser Abhängigkeit werden wir anstelle von Mockito Mockk verwenden, um Mock-Instanzen zu erstellen.
// ... other dependencies already included
testImplementation("com.ninja-squad:springmockk:3.0.1")
Diese Bibliothek hilft uns, Mockk im Spring-Kontext besser zu unterstützen. Sie können hier mehr darüber lesen.
Erstellen Sie eine neue Klasse namens PaymentsControllerTest
im Testordner. Nehmen Sie sich einen Moment Zeit, um die untenstehenden Tests anzusehen, und wir werden verstehen, was passiert.
package com.backendhorizon.bank.controller
import com.fasterxml.jackson.databind.ObjectMapper
import com.backendhorizon.bank.controller.model.TransactionModel
import com.backendhorizon.bank.repository.TransferRepository
import com.backendhorizon.bank.repository.model.TransactionDBModel
import com.ninjasquad.springmockk.MockkBean
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
import java.util.*
@WebMvcTest
class PaymentsControllerTest(@Autowired val mockMvc: MockMvc) {
@MockkBean
private lateinit var paymentsRepository: TransferRepository
@Test
fun `should submit transaction with success`() {
val transfer = TransactionModel(
value = 1.50,
description = "store 1",
targetAccount = "NL76ABNA2376059879"
)
every { paymentsRepository.save(any()) } returns mockk()
mockMvc.perform(post("/transfer/new")
.content(ObjectMapper().writeValueAsString(transfer))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk)
}
@Test
fun `should get transaction with success`() {
val mockTransaction = mockk<TransactionDBModel>().apply {
every { value } returns 1.50
every { description } returns "store 1"
every { accountIdentifier } returns "NL47INGB8845464385"
every { date } returns Date()
every { id } returns UUID.randomUUID()
}
every { paymentsRepository.findAll() } returns listOf(mockTransaction)
mockMvc.perform(get("/transfer/all").accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("\$.[0].description").value("store 1"))
}
@Test
fun `should return a bad request error if the request there's no body`() {
mockMvc.perform(post("/transfer/new")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().is4xxClientError)
}
}
Die allgemeine Idee, was die Tests tun, ist dank des Spring Testing-Frameworks leicht zu verstehen, da es sehr ausdrucksstark ist. Schauen wir uns die technischen Details an:
@WebMvcTest
: Diese Annotation zeigt an, dass diese Tests eine MVC-Komponente testen, in diesem Fall einen REST-Controller.MockMVC
: Dies ist der Einstiegspunkt, der es uns ermöglicht, während der Tests mit unserem Controller zu kommunizieren, Anfragen zu stellen und das Ergebnis sowie die Statuscodes zu überprüfen.@MockkBean
: Wir mocken eine Fake-TransferRepository
-Klasse, um verschiedene Szenarien zu simulieren. Wir simulieren diese Szenarien immer, bevor wir eine Aktion mitMockMvc
ausführen, indem wir das Ergebnis derfindAll
- undsave
-Methoden mocken.
Nachdem wir unsere Mocks eingerichtet haben, stellen wir eine Anfrage an den Controller mithilfe von MockMvc
. Alle Tests folgen einer ähnlichen Struktur:
- Wir geben
MockMvc
an, eine Anfrage auszuführen und welche Art es ist (post oder get). - Wenn wir einen Body in der Anfrage haben, transformieren wir das Objekt in ein JSON.
- Der Inhaltstyp der Anfrage,
APPLICATION_JSON
. - Wir drucken die Anfragedetails und Ergebnisse in der Konsole aus.
- Und schließlich geben wir an, was wir von dieser Anfrage erwarten. Es kann eine Statuscodeüberprüfung sein, ob die Anfrage erfolgreich ist, oder eine JSON-Antwort (und deren Inhalt).
Und schließlich sind wir fertig. 🎉🎉🎉
Du kannst das vollständige Projekt auf GitHub finden.
Fragen oder Vorschläge? Bitte hinterlassen Sie einen Kommentar, wir sind alle hier, um gemeinsam zu lernen. 🤠
Danke fürs Lesen.