Dans cet article, vous apprendrez comment créer votre première API avec Spring Boot et Kotlin.
Spring Framework
Spring Framework 🍃, le framework le plus populaire en Java pour le développement d’applications. Des millions de développeurs utilisent Spring chaque jour pour créer toutes sortes d’applications. La première version de Spring en 2003 était axée sur l’injection de dépendances mais le framework a énormément grandi, l’équipe de Spring a créé un ensemble de modules qui supportent presque tout ce dont un développeur a besoin pour créer une application. Étant donné cette maturité, il n’est pas étonnant que le framework soit utilisé dans les plus grandes entreprises technologiques du monde. C’est l’avantage d’un projet qui est en développement et amélioration constants depuis presque 20 ans.
Qu’est-ce que Kotlin
Kotlin est un langage créé par JetBrains, la même entreprise qui a créé tous les IDE incroyables comme IntelliJ Idea. JetBrains n’était pas satisfait de Java et des autres langages sur le marché et a décidé de créer Kotlin pour être une amélioration de Java. Et quand je dis amélioration, c’est parce que Kotlin a été conçu pour être totalement compatible avec Java, rendant possible d’avoir une base de code partagée entre ces deux langages, permettant aux entreprises de faire une migration progressive de Java.
Kotlin est en train de devenir très populaire parmi les développeurs et selon le sondage StackOverflow 2020, Kotlin est l’un des langages les plus appréciés. Tout cet amour ne vient pas de nulle part. Kotlin a beaucoup de fonctionnalités qui rendent le développement plus facile, moins verbeux et plus résistant aux erreurs. L’une des meilleures fonctionnalités est un système de sécurité nul natif, des classes d’extensions, des fonctions d’ordre supérieur et la version async await du langage, appelée coroutines.
Création de projet
Pour commencer à créer notre projet, nous utiliserons l’outil Spring Initializer. Vous pouvez le trouver sur start.spring.io. C’est un outil créé par l’équipe Spring et il est très utile car nous pouvons déjà faire notre configuration initiale et inclure nos principales dépendances.
Tout d’abord, vous pouvez trouver le code source complet ici sur mon Github. Pour ce projet, nous utiliserons une configuration un peu différente de l’autre tutoriel courant que nous pouvons trouver avec Spring Boot. Nous allons utiliser Gradle comme outil de build, mais bien sûr, comme vous pouvez le voir, Maven est également une option. Nous choisirons la version la plus stable de Spring, c’est-à-dire 2.6.0 (au moment où j’écris cet article). Vous pouvez changer les métadonnées du projet si vous le souhaitez ou simplement laisser les valeurs par défaut. J’ai choisi Java 11 car je n’ai pas encore installé Java 17, mais cela ne fera aucune différence pour ce projet.
Dans la colonne de droite, nous avons l’option d’inclure nos dépendances initiales. Pour ce projet, nous inclurons quelques-unes d’entre elles et plus tard, nous ajouterons d’autres dépendances manuellement dans notre fichier Gradle. Commençons par inclure :
- Spring Boot DevTools — Il y a des fonctionnalités intéressantes qui peuvent nous aider pendant le développement comme le rechargement automatique, le rechargement en direct et la désactivation du cache. Très utile pendant le développement.
- Spring Web — Nous en aurons besoin pour créer nos API
- Spring JPA et H2 Database — pour sauvegarder nos données
Comme vous pouvez le voir dans mes métadonnées, je vais créer un exemple simulant une banque et nous allons créer une API de transfert. 💰
IntelliJ Idea
Décompressez le fichier et ouvrez-le dans votre IDE. Je vais utiliser IntelliJ Idea Community Edition. Vous pouvez le télécharger gratuitement ou utiliser un autre IDE. Il est intéressant de mentionner que si vous avez la version Ultimate d’IntelliJ Idea, l’outil Spring Initialiser que nous venons de voir est déjà intégré dans l’IDE, vous pouvez donc effectuer toute la création du projet directement là-bas.
Gradle file
La première chose que nous allons examiner est notre fichier build.gradle.kts
. Vous pouvez le trouver à la racine du dossier du projet. C’est le fichier où nous pouvons gérer nos dépendances et comme nous avons utilisé Spring Initializer pour la configuration initiale, nous pouvons voir que l’outil a créé ce fichier avec notre configuration et les dépendances sélectionnées.
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()
}
Puisque nous avons choisi d’utiliser Kotlin, Spring Initializer a déjà créé ce fichier de configuration en Kotlin. Pour les personnes qui ne sont pas familières avec Gradle, je souligne cela car il est également possible d’écrire vos fichiers Gradle avec Groovy. Puisque nous utilisons déjà Kotlin, je recommande fortement de l’utiliser également avec Gradle car il y a certains avantages par rapport à Groovy, comme par exemple l’auto-complétion.
Plugins Gradle
Jetons un coup d’œil au bloc des plugins. Gradle lui-même ne fait pas grand-chose par défaut, mais il offre aux développeurs des options pour étendre et ajouter de nouvelles fonctionnalités à nos builds. Un excellent exemple de ceci est le bloc “plugins” où nous avons des plugins qui configurent et laissent notre projet prêt à l’emploi très facilement.
Nous pouvons voir que nous avons quelques plugins et la plupart d’entre eux sont pour une configuration initiale de Spring et Kotlin. Jetons un coup d’œil à la page du plugin Spring Boot, et voyons comment le plugin est décrit :
Spring Boot facilite la création d’applications autonomes, de qualité production, basées sur Spring que vous pouvez “simplement exécuter”. Nous adoptons une vue systémique de la plateforme Spring et des bibliothèques tierces afin que vous puissiez démarrer avec un minimum de tracas. La plupart des applications Spring Boot nécessitent très peu de configuration Spring.
En d’autres termes, une configuration facile pour commencer à travailler sur votre projet immédiatement. Tous les autres plugins font quelque chose de similaire, ils préparent et aident à gérer les dépendances de notre projet pour qu’elles soient configurées et intégrées de la meilleure façon.
Sous les plugins, nous avons les métadonnées que nous choisissons et la version de Java.
Maven Central est le référentiel d’où nous téléchargeons nos dépendances et sous le bloc dependencies, toutes les bibliothèques que nous avons dans le projet sont listées. Plus tard, nous reviendrons sur ce fichier et nous allons inclure des dépendances supplémentaires.
Il y a un autre fichier important qui vaut la peine d’être examiné rapidement, c’est notre fichier Application.
Fichier de l’application
Ouvrons notre fichier de l’application. Dans le projet sur GitHub, vous pouvez trouver ce fichier intitulé BankApplication
mais cela changera en fonction de ce que vous choisissez dans vos métadonnées. Ici, nous trouvons la fonction la plus célèbre de toutes, la fonction principale, où l’application commence.
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)
}
On peut voir que dans la méthode principale, nous avons cette fonction runApplication
indiquant de s’exécuter avec la classe BankApplication
et dans cette classe, nous avons juste une seule annotation, @SpringBootApplication
. Cette seule annotation fait beaucoup pour nous.
Jetons un coup d’œil rapide à la manière dont la documentation la décrit :
Indique une classe de configuration qui déclare une ou plusieurs méthodes
@Bean
et déclenche également l’auto-configuration et la vérification des composants. C’est une annotation pratique qui équivaut à déclarer@Configuration
,@EnableAutoConfiguration
et@ComponentScan
.
Dans le prochain chapitre, nous créerons notre premier contrôleur et quelque chose que vous remarquerez est que nous ne créerons pas une instance du contrôleur. C’est parce que l’annotation @SpringBootApplication
est composée de 3 autres annotations et l’une d’elles est @ComponentScan
.
Comme le nom le suggère déjà, @ComponentScan
va parcourir notre projet à la recherche de composants et quand il les trouvera, Spring créera et gérera cette instance pour nous. Il est à noter que pour qu’un composant soit auto-détecté, il doit être au même niveau de package ou en dessous de la classe d’application.
Les composants sont des classes annotées avec des annotations spéciales qui les candidatisent à être auto-détectées. Il existe quelques annotations différentes qui peuvent être utilisées à des fins différentes comme @Component
, @Controller
, @RestController
, @Repository
et @Service
.
L’annotation @EnableAutoConfiguration
fera quelque chose de similaire pour les bibliothèques dans le projet. Elle recherchera dans notre classpath, vérifiera les bibliothèques que nous utilisons et chargera une configuration initiale pour nous. Nous verrons cela avec notre base de données. Continuez à lire. 😉
Première API
Créons notre premier contrôleur d’API. Créez un package appelé controller et à l’intérieur, une classe appelée TransferController
. Créez 2 méthodes, newTransfer
et getAllTransfers
comme dans l’exemple ci-dessous.
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"
}
}
Comprenons ce qui se passe ici. En haut de notre classe TransferController
, nous avons utilisé 2 annotations, @RestController
et @RequestMapping
:
@RestController
: En utilisant cette annotation, nous indiquons à Spring Boot que cette classe est un web handler/web controller. Elle recevra des requêtes et renverra des réponses aux appelants. Puisqu’il s’agit d’un type de “controller” cette component (gardez ce terme à l’esprit) sera également gérée par Spring Boot.@RequestMapping
: Cela indique quelle sera la signature de ce web controller dans l’adresse de la requête, après l’adresse de l’hôte. Par exemple, nos 2 méthodes endpoints auront les adresses suivantes :localhost:8080/transfer/new
etlocahost:8080/transfer/all
.
En suivant les méthodes, elles sont relativement simples :
newTransfer
enregistre un message “Saved” dans la console et son annotation indique qu’il s’agira d’une requête de type “Post”,@PostMapping
et comme le controller, elle indique quelle est la signature pour cette méthode,localhost:8080/payment/new
.getAllTransfer
retourne un message “Hello” à l’appelant et, similaire à la méthode précédente, elle indique le type de la requête, dans ce cas, une requête de type Get,@GetMapping
, suivie de la signature,localhost:8080/payment/all
.Get
etPost
font partie de la spécification REST, si vous n’êtes pas familier avec eux, laissez un commentaire et je créerai un nouvel article les expliquant plus en détail.
Exécution et Test
Nous avons maintenant tout ce dont nous avons besoin pour effectuer notre premier test. Allez dans la fonction principale du fichier de l’application et exécutez votre application. Vous devriez commencer à voir des journaux et Tomcat, notre WebServer, démarrera et hébergera l’application sans aucun problème. Remarquez dans les journaux que Tomcat vous indiquera sur quel port votre application fonctionne.
Nous devons maintenant tester ces endpoints. Le meilleur moyen est d’utiliser un outil prévu à cet effet. Personnellement, je recommande Postman. Il prend en charge les API REST, ce qui permet de tester tous les différents types d’endpoints REST, de configurer les corps JSON, les en-têtes, les jetons d’autorisation, etc. Postman prend également en charge GraphQL si vous aimez ça. 😂
Ouvrez un nouvel onglet sur Postman, sélectionnez GET et tapez l’adresse de l’endpoint getAllTransfers
, comme sur l’image ci-dessous :
Comme vous pouvez le voir, nous avons reçu un message “Hello” en retour. Notre API fonctionne ! Faites de même pour newTransfer
, changez l’URL et le type en POST. Vous verrez que l’API fonctionne également et que le message “Saved” est imprimé sur la console IntelliJ Idea.
Remarquez que nous n’avons créé aucune instance de TransferController, juste l’annotation @RestController a indiqué à Spring que notre contrôleur devait être créé. Ça marche à merveille, non ?!
Modèles de Données
Commençons par préparer notre application à recevoir et sauvegarder des données. À l’intérieur du package controller, créez-en un nouveau appelé model
. Créez deux nouvelles classes, TransactionModel
et OverviewTransactionModel
.
Dans la classe TransactionModel
, nous allons créer quelques attributs. Imaginons que pour effectuer un transfert à une autre personne, nous devons connaître quel est l’identifiant du compte, qui peut être un numéro ou une chaîne, selon le pays dans lequel vous vivez. Nous avons également besoin de la valeur et d’une description qui peut être optionnelle.
package com.backendhorizon.bank.controller.model
class TransactionModel(
val targetAccount: String,
val value: Double,
val description: String = "",
)
Lorsque nous récupérons la liste de toutes les transactions, nous aimerions voir un peu plus de données. En plus de toutes les données précédentes, nous aurons deux champs supplémentaires, la date de la transaction et l’identifiant de transaction (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
)
Mettez à jour votre TransferController
comme le code ci-dessous avec les nouveaux modèles que nous avons créés. Vous pouvez remarquer que dans la méthode newTransfer
nous avons une nouvelle annotation @RequestBody
. Cela indique que le TransactionModel
fait partie de cette requête et sera désérialisé automatiquement en notre modèle.
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()
}
}
Base de données
Maintenant que nous recevons des données via notre API, nous devons les stocker dans une base de données. Nous allons utiliser une base de données en mémoire appelée H2 que nous avons déjà incluse dans notre fichier build.gradle.kts
.
Créez un nouveau package au même niveau que le contrôleur appelé repository
. Dans ce package, une interface appelée TransferRepository
. Créons également un autre package model
à l’intérieur de repository et, à l’intérieur, une classe appelée TransactionDBModel
. À la fin, vous devriez avoir quelque chose comme ceci :
Tout d’abord, travaillons sur les attributs du modèle DB. Similaire à TransactionModel
, nous aurons l’identifiant du compte, la valeur, et la description. Pour enregistrer dans la base de données, nous devons avoir un identifiant unique et cet identifiant sera généré automatiquement pour nous. Nous allons également utiliser la date actuelle comme date de la transaction.
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()
}
Par constructeur, nous allons recevoir les attributs que nous recevons via notre API et dans le corps de la classe, nous aurons les deux attributs supplémentaires (id et date). Nous pourrions déjà initialiser l’id avec un UUID aléatoire, mais nous laisserons JPA le faire. Nous rendons le set privé afin de ne pas pouvoir remplacer cette valeur, juste la lire. La date sera toujours la date actuelle, donc nous l’initialisons simplement avec la date actuelle.
Dans cette classe, nous avons trois nouvelles annotations :
@Entity
: Cela indique que cette classe sera utilisée par JPA (Java Persistence API) pour sauvegarder les données dans une base de données.@Id
: Indique que le champ annoté sera la clé primaire dans la base de données pour cette entité.@GeneratedValue
: le nom est explicite. Il indique que cette clé primaire doit s’auto-générer.
Maintenant que nous avons notre modèle, nous allons créer notre interface qui fera les opérations CRUD (Create, Read, Update and Delete). La façon dont nous allons faire cela est en étendant notre TransferRepository
avec CrudRepository
. C’est incroyable à quel point c’est simple et puissant.
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>
Si vous ouvrez l’interface CrudRepository
, il est facile de voir ce qui se passe. Cette interface contient toutes les fonctionnalités génériques pour CRUD et tout ce que nous devons faire est d’indiquer quel type est notre modèle et la clé primaire, dans notre cas TransactionDBModel
et UUID
.
En indiquant simplement les types de modèle et de clé primaire, Spring saura que nous utilisons cette interface et injectera une implémentation par défaut pour cette interface (et les types que nous fournissons). Vous pouvez jeter un œil à l’implémentation par défaut, c’est une classe appelée SimpleJpaRepository
.
Bien sûr, nous ne sommes pas limités à ces seules fonctions. Il est également possible de créer des opérations de base de données plus complexes, mais pour cet article, nous travaillerons uniquement avec cette implémentation par défaut.
De l’API à la base de données et vice-versa
Maintenant que notre couche de persistance est terminée, nous pouvons enregistrer les données que nous recevons de l’API. La seule chose que nous devons faire est une petite conversion entre nos modèles, car le modèle que nous recevons dans le point de terminaison est différent de celui que nous utilisons dans la base de données. Créons deux fonctions d’extension pour convertir ces modèles, convertToDBModel
et convertToOverviewTransactionModel
.
D’abord dans TransactionModel
, convertissez le modèle de point de terminaison en un modèle de base de données, 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
)
Et pour l’aperçu, quelque chose de similaire. Mais cette fois, le modèle de base de données TransactionDBModel
en un modèle de point de terminaison, 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, maintenant nous pouvons convertir les données entre nos points de terminaison et la base de données. Mettez à jour notre contrôleur pour effectuer ces appels.
Dans le constructeur, nous inclurons notre classe TransferRepository
. N’oubliez pas, Spring est intelligent, lorsque Spring voit que nous avons cette dépendance dans notre contrôleur, Spring injectera l’implémentation créée pour TransactionRepository
(basée sur SimpleJpaRepository
).
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() }
}
}
Vous pouvez voir dans l’exemple ci-dessus que notre classe repository utilise les méthodes génériques de CrudRepository
pour sauvegarder et trouver tous les transactions dans la base de données. Nous utilisons également les méthodes d’extension que nous avons créées pour convertir le modèle au plus approprié.
À ce stade, nos endpoints sont tous terminés. Recompilez et exécutez l’application. Créons une nouvelle demande de transfert. Rappelez-vous que nous devons maintenant inclure toutes les données nécessaires sur Postman pour que notre endpoint puisse fonctionner correctement. Incluez un corps JSON comme sur l’image ci-dessous et soumettez.
Après la soumission, tout devrait bien fonctionner. Vous devriez recevoir un statut 200, ce qui signifie que la demande a réussi. Maintenant, si vous récupérez toutes les données à nouveau, vous verrez les données de transfert soumises, y compris la date et l’identifiant généré.
Notre API fonctionne 🎉 mais nous n’avons pas encore terminé !
Console de base de données
Il y a un moyen très pratique que H2 Database nous offre pour vérifier les données sauvegardées et c’est la “H2 Console”. C’est une interface web où vous pouvez vérifier les données de la base de données et exécuter des requêtes.
Tout d’abord, pour activer cette console, vous devez ajouter le code suivant dans votre fichier application.properties
à l’intérieur du dossier ressource.
# Enabling H2 Console
spring.h2.console.enabled=true
Après avoir ajouté, exécutez à nouveau votre application et entrez dans votre navigateur l’URL http://localhost:8080/h2-console
. Vous allez voir une fenêtre de connexion pour vous connecter à votre base de données. La seule chose que vous devez changer est l’URL JDBC. Regardez dans vos journaux de console et recherchez “H2 console available at …” comme dans l’image ci-dessous.
Copiez l’URL de la base de données et appuyez sur connect. Vous devriez voir la console. Maintenant, vous pouvez jouer avec votre base de données, exécuter des requêtes SQL et voir les données enregistrées depuis votre API.
Maintenant, nous avons terminé, n’est-ce pas ? FAUX ! Nous sommes de bons développeurs et les bons développeurs écrivent des tests !
Tests unitaires
Avant d’écrire notre premier test, ajoutons une nouvelle dépendance dans le bloc des dépendances à l’intérieur de build.gradle.kts
. Avec cette dépendance, au lieu de Mockito, nous allons utiliser Mockk pour créer des instances factices.
// ... other dependencies already included
testImplementation("com.ninja-squad:springmockk:3.0.1")
Cette bibliothèque nous aide à mieux supporter Mockk dans le contexte de Spring. Vous pouvez en lire plus ici.
Créez une nouvelle classe appelée PaymentsControllerTest
dans le dossier tests. Prenez un moment pour regarder les tests ci-dessous et nous comprendrons ce qui se passe.
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)
}
}
L’idée générale de ce que font les tests est facile à comprendre grâce au framework de tests Spring, qui est très expressif. Voyons les détails techniques :
@WebMvcTest
: Cette annotation indique que ces tests testent un composant MVC, dans ce cas, un contrôleur REST.MockMVC
: C’est le point d’entrée qui nous permettra de communiquer avec notre contrôleur pendant les tests, en faisant des requêtes et en vérifiant le résultat et les statuts.@MockkBean
: Nous simulons une classeTransferRepository
factice pour simuler différents scénarios. Nous simulons toujours ces scénarios avant d’effectuer une action avecMockMvc
, en simulant le résultat des méthodesfindAll
etsave
.
Après avoir configuré nos mocks, nous effectuons une requête au contrôleur en utilisant MockMvc
. Tous les tests suivent une structure similaire :
- Nous demandons à
MockMvc
d’effectuer une requête et son type (post ou get). - Si nous avons un corps dans la requête, nous transformons l’objet en JSON.
- Le type de contenu de la requête,
APPLICATION_JSON
. - Nous imprimons dans la console les détails et les résultats de la requête.
- Et enfin, nous déclarons ce que nous attendons de cette requête. Cela peut être un code de statut vérifiant si la requête réussit ou une réponse JSON (et son contenu).
Et nous avons enfin terminé. 🎉🎉🎉
Vous pouvez trouver le projet complet sur GitHub.
Questions ou suggestions, veuillez laisser un commentaire, nous sommes tous ici pour apprendre ensemble. 🤠
Merci de votre lecture.