Vedi post per tag

Vedi post per categorie

Spring Boot con Kotlin — Creare la tua Prima API

Spring Boot con Kotlin — Creare la tua Prima API
SHARE

In questo articolo imparerai come creare la tua prima API con Spring Boot e Kotlin.

Spring Framework

Spring Framework 🍃, il framework più popolare in Java per lo sviluppo di applicazioni. Milioni di sviluppatori utilizzano Spring ogni giorno per creare ogni tipo di applicazione. La prima versione di Spring nel 2003 si concentrava sull’iniezione delle dipendenze, ma il framework è cresciuto immensamente, il team di spring ha creato un insieme di moduli che supportano quasi tutto ciò di cui uno sviluppatore ha bisogno per creare un’applicazione. Data tanta maturità, non è sorprendente che il framework sia utilizzato nelle migliori aziende tecnologiche del mondo. Questo è il vantaggio di un progetto che è stato in costante sviluppo e miglioramento per quasi 20 anni.

Cos’è Kotlin

Kotlin è un linguaggio creato da Jetbrains, la stessa azienda che ha creato tutti i fantastici IDE come IntelliJ Idea. JetBrains non era soddisfatta di Java e degli altri linguaggi sul mercato e ha deciso di creare Kotlin come miglioramento di Java. E quando dico miglioramento, è perché Kotlin è stato progettato per essere completamente compatibile con Java, rendendo possibile avere una base di codice condivisa tra questi due linguaggi, permettendo alle aziende di effettuare una migrazione graduale da Java.

Kotlin sta diventando molto popolare tra gli sviluppatori e, secondo il sondaggio StackOverflow 2020, Kotlin è uno dei linguaggi più amati. Tutto questo amore non è senza motivo. Kotlin ha molte funzionalità che rendono lo sviluppo più semplice, meno verboso e più sicuro. Uno dei migliori funzionalità è un sistema nativo di sicurezza per i null, classi di estensioni, funzioni di ordine superiore e la versione async await del linguaggio, chiamata coroutines.

Creare progetto

Per iniziare a creare il nostro progetto useremo lo strumento Spring Initializer. Puoi trovarlo su start.spring.io. Questo è uno strumento creato dal team di spring ed è super utile perché possiamo già fare la nostra configurazione iniziale e includere le nostre dipendenze principali.

Schermata di Spring initializer

Per prima cosa, puoi trovare il codice sorgente completo qui sul mio Github. Per questo progetto, useremo una configurazione un po’ diversa dagli altri tutorial comuni che possiamo trovare con Spring Boot. Usiamo Gradle come il nostro strumento di build ma, ovviamente, come puoi vedere Maven è anche un’opzione. Sceglieremo la versione più stabile di Spring, ovvero 2.6.0 (quando sto scrivendo questo articolo). Puoi cambiare i metadata del progetto se vuoi oppure lasciare i valori predefiniti. Ho scelto Java 11 perché non ho ancora installato Java 17 ma non farà alcuna differenza per questo progetto.

Nella colonna di destra, abbiamo la possibilità di includere le nostre dipendenze iniziali. Per questo progetto, ne includeremo alcune e successivamente aggiungeremo altre dipendenze manualmente nel nostro file Gradle. Iniziamo includendo:

  • Spring Boot DevTools — Ci sono alcune funzionalità carine che possono aiutarci durante lo sviluppo come il ricaricamento automatico, il live reload e la disabilitazione della cache. Molto utile durante lo sviluppo.
  • Spring Web — Avremo bisogno di creare le nostre API
  • Spring JPA e H2 Database — per salvare i nostri dati

Dipendenze di Spring initializer

Come puoi vedere nei miei metadata, creerò un esempio simulando una banca e faremo un’API di trasferimento. 💰

IntelliJ Idea

Decomprimi il file e aprilo nel tuo IDE. Userò IntelliJ Idea Community Edition. Puoi scaricarlo gratuitamente o utilizzare un altro IDE. Una cosa interessante da menzionare è che se hai la versione ultimate di IntelliJ Idea, lo Spring Initialiser Tool che abbiamo appena visto è già integrato nell’IDE, quindi puoi fare tutta la creazione del progetto direttamente lì.

Gradle file

La prima cosa che esamineremo è il nostro file build.gradle.kts. Puoi trovarlo nella radice della cartella del progetto. Questo è il file dove possiamo gestire le nostre dipendenze e, poiché abbiamo utilizzato Spring Initializer per la configurazione iniziale, possiamo vedere che lo strumento ha creato questo file con la nostra configurazione e le dipendenze selezionate.

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()
}

Poiché abbiamo scelto di usare Kotlin, Spring Initializer ha già creato questo file di configurazione come file Kotlin. Per le persone che non sono familiari con Gradle, lo sottolineo perché è anche possibile scrivere i file Gradle con Groovy. Poiché stiamo già usando Kotlin, consiglio vivamente di usarlo anche con Gradle poiché ci sono alcuni vantaggi rispetto a Groovy, come l’auto-completamento, per esempio.

Gradle Plugins

Diamo un’occhiata al blocco dei plugin. Gradle di per sé non fa molto di default, ma offre opzioni a noi sviluppatori per estendere e aggiungere nuove funzionalità alle nostre build. Un ottimo esempio è il blocco “plugins” dove abbiamo plugin che configurano e rendono il nostro progetto pronto all’uso molto facilmente.

Possiamo vedere che abbiamo alcuni plugin e la maggior parte di essi serve per una configurazione iniziale per Spring e Kotlin. Diamo un’occhiata alla Spring Boot Plugin page e vediamo come viene descritto il plugin:

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that can you can “just run”. We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need very little Spring configuration.

In altre parole, configurazione facile per iniziare a lavorare sul tuo progetto immediatamente. Tutti gli altri plugin fanno qualcosa di simile, preparano e aiutano a gestire le nostre dipendenze del progetto affinché siano configurate e integrate nel miglior modo possibile.

Sotto i plugin, abbiamo i metadati che scegliamo e la versione di Java.

Maven Central è il repository da cui stiamo scaricando le nostre dipendenze e sotto il blocco delle dipendenze sono elencate tutte le librerie che abbiamo nel progetto. Più tardi torneremo a questo file e includeremo alcune dipendenze extra.

C’è un altro file importante che vale la pena dare un’occhiata rapida, ed è il nostro file Application.

Application file

Apriamo il nostro file Application. Nel progetto su GitHub, puoi trovare questo file intitolato come BankApplication ma questo cambierà a seconda di ciò che scegli nei tuoi metadata. Qui troviamo la funzione più famosa di tutte, la funzione main, dove inizia l’applicazione.

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)
}

Possiamo vedere che nel metodo main abbiamo questa funzione runApplication che indica di eseguire con la classe BankApplication e in questa classe abbiamo solo una singola annotazione, @SpringBootApplication. Questa singola annotazione fa molto per noi.

Diamo un’occhiata rapida a come la documentazione la descrive:

Indica una classe di configurazione che dichiara uno o più metodi @Bean e attiva anche la configurazione automatica e la scansione dei componenti. Questa è un’annotazione di convenienza che è equivalente a dichiarare @Configuration, @EnableAutoConfiguration e @ComponentScan.

Nel prossimo capitolo, creeremo il nostro primo Controller e una cosa che noterai è che non creeremo un’istanza del controller. Questo perché l’annotazione @SpringBootApplication è composta da altre 3 annotazioni e una di queste è @ComponentScan.

Come il nome stesso suggerisce, @ComponentScan cercherà/scansionerà il nostro progetto alla ricerca di componenti e quando li trova, Spring creerà e gestirà questa istanza per noi. Una cosa interessante da menzionare è che affinché un componente venga rilevato automaticamente deve trovarsi allo stesso livello di pacchetto o inferiore della classe dell’applicazione.

I componenti sono classi annotate con annotazioni speciali che candidano queste classi per essere rilevate automaticamente. Ci sono alcune diverse annotazioni che possono essere utilizzate per scopi diversi come @Component, @Controller, @RestController, @Repository e @Service.

L’annotazione @EnableAutoConfiguration farà qualcosa di simile per le librerie nel progetto. Cercherà nel nostro classpath, controllerà le librerie che stiamo utilizzando e caricherà una configurazione iniziale per noi. Lo vedremo con il nostro database. Continua a leggere. 😉

Prima API

Creiamo il nostro primo controller API. Crea un pacchetto chiamato controller e, al suo interno, una classe chiamata TransferController. Crea 2 metodi, newTransfer e getAllTransfers, come nell’esempio qui sotto.

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"
    }

}

Capiremo cosa sta succedendo qui. In cima alla nostra classe TransferController, abbiamo utilizzato 2 annotazioni, @RestController e @RequestMapping:

  • @RestController: Usando questa annotazione indichiamo a Spring Boot che questa classe è un gestore web/controller web. Riceverà richieste e restituirà risposte ai chiamanti. Poiché è un tipo di “controller” questo componente (tieni presente questo termine) sarà gestito anche da Spring Boot.
  • @RequestMapping: Questo indica quale sarà la firma di questo controller web nell’indirizzo della richiesta, dopo l’indirizzo dell’host. Ad esempio, i nostri 2 metodi endpoint avranno i seguenti indirizzi: localhost:8080/transfer/new e localhost:8080/transfer/all.

Seguendo i metodi, sono relativamente semplici:

  • newTransfer registra un messaggio “Saved” nella console e la sua annotazione indica che questa sarà una richiesta di tipo “Post”, @PostMapping e come il controller indica qual è la firma per questo metodo, localhost:8080/payment/new.
  • getAllTransfer restituisce un messaggio “Hello” al chiamante e, simile al metodo precedente, indica il tipo di richiesta, in questo caso, una richiesta di tipo Get, @GetMapping, seguita dalla firma, localhost:8080/payment/all.
  • Get e Post fanno parte della specifica REST, se non sei familiare con loro, lascia un commento e creerò un nuovo articolo che li spiega più in dettaglio.

Esecuzione & Testing

Ora abbiamo tutto il necessario per fare il nostro primo test. Vai alla main function nel file dell’applicazione e avvia la tua applicazione. Dovresti iniziare a vedere alcuni log e Tomcat, il nostro WebServer, avvierà e ospiterà l’applicazione senza alcun problema. Nota nei log che Tomcat ti dirà quale porta sta utilizzando la tua applicazione.

Tomcat logs

Ora dobbiamo testare questi endpoint. Il modo migliore è utilizzare uno strumento appositamente creato per questo. Personalmente raccomando Postman. Ha il pieno supporto per le REST API, rendendo possibile il test di tutti i diversi tipi di endpoint REST, configurando corpi JSON, intestazioni, token di autorizzazione, ecc. Postman supporta anche GraphQL nel caso tu sia sofisticato. 😂

Apri una nuova scheda su Postman, seleziona GET e digita l’indirizzo dell’endpoint getAllTransfers, come nell’immagine qui sotto:

Postman screen

Come puoi vedere, abbiamo ricevuto in risposta un messaggio di “Hello”. La nostra API funziona! Fai lo stesso per newTransfer, cambia l’URL e il tipo in POST. Vedrai che l’API funziona anche e il messaggio “Saved” è stampato sulla console di IntelliJ Idea.

Nota che non abbiamo creato alcuna istanza di TransferController, solo l’annotazione @RestController ha indicato a Spring che il nostro controller doveva essere creato. Funziona a meraviglia, vero?!

Data Models

Iniziamo a preparare la nostra applicazione per ricevere e salvare dati. All’interno del pacchetto controller crea uno nuovo chiamato model. Crea due nuove classi, TransactionModel e OverviewTransactionModel.

Nella classe TransactionModel andremo a creare alcuni attributi. Quindi immaginiamo che per effettuare un trasferimento a un’altra persona dobbiamo conoscere qual è l’identificatore dell’account, che può essere un numero o una stringa, a seconda del paese in cui ti trovi. Abbiamo anche bisogno del valore e di una descrizione che può essere facoltativa.

package com.backendhorizon.bank.controller.model

class TransactionModel(
    val targetAccount: String,
    val value: Double,
    val description: String = "",
)

Quando fetchiamo l’elenco di tutte le transazioni, vorremmo vedere un po’ più di dati. Oltre a tutti i dati precedenti, avremo due campi extra, la data della transazione e l’identificatore della transazione (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
)

Aggiorna il tuo TransferController come nel codice seguente con i nuovi modelli che abbiamo creato. Puoi notare che nel metodo newTransfer abbiamo una nuova annotazione @RequestBody. Questo indica che il TransactionModel fa parte di questa richiesta e deserializzerà automaticamente il JSON nel nostro modello.

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()
    }

}

Database

Ora che stiamo ricevendo dati tramite la nostra API, dobbiamo memorizzarli in un database. Usiamo un database in-memory chiamato H2 che abbiamo già incluso nel nostro file build.gradle.kts.

Crea un nuovo pacchetto allo stesso livello del controller chiamato repository. All’interno di questo pacchetto, un’interfaccia chiamata TransferRepository. Creiamo anche un altro pacchetto model all’interno di repository e in esso una classe chiamata TransactionDBModel. Alla fine, dovresti avere qualcosa di simile a questo:

Struttura del progetto

Per prima cosa, lavoriamo sugli attributi del modello DB. Simile a TransactionModel avremo l’identificatore del conto, il valore e la descrizione. Per salvare nel database, dobbiamo avere un identificatore univoco e questo identificatore verrà generato automaticamente per noi. Utilizzeremo anche la data corrente come data della transazione.

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()
}

Per costruttore, riceveremo gli attributi che riceviamo tramite la nostra API e nel corpo della classe, avremo i due attributi extra (id e date). Potremmo già inizializzare l’id con un UUID casuale ma lo lasceremo fare a JPA. Rendiamo il set privato in modo che non possiamo sovrascrivere questo valore, solo leggerlo. La data sarà sempre quella corrente, quindi la inizializziamo semplicemente con quella corrente.

In questa classe abbiamo tre nuove annotazioni:

  • @Entity: Questo indica che questa classe sarà utilizzata da JPA (Java Persistence API) per salvare i dati in un database.
  • @Id: Indica che il campo annotato sarà la chiave primaria nel database per questa entità.
  • @GeneratedValue: il nome è autoesplicativo. Indica che questa chiave primaria dovrebbe autogenerare il proprio valore.

Ora che abbiamo il nostro modello, creeremo la nostra interfaccia che eseguirà operazioni CRUD (Create, Read, Update e Delete). Il modo in cui faremo questo è estendendo il nostro TransferRepository con CrudRepository. È sorprendente quanto sia semplice e potente.

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>

Se apri l’interfaccia CrudRepository è facile capire cosa sta succedendo. Questa interfaccia ha tutte le funzionalità generiche per CRUD e tutto ciò di cui abbiamo bisogno è indicare quale tipo è il nostro modello e la chiave primaria, nel nostro caso TransactionDBModel e UUID.

Solo indicando i tipi di modello e chiave primaria, Spring capirà che stiamo usando questa interfaccia e inietterà una implementazione predefinita per questa interfaccia (e i tipi che forniamo). Puoi dare un’occhiata all’implementazione predefinita, è una classe chiamata SimpleJpaRepository.

Naturalmente, non siamo limitati solo a queste funzioni. È anche possibile creare operazioni di database più complesse, ma per questo articolo, lavoreremo solo con questa implementazione predefinita.

Dall’API al database e viceversa

Ora che il nostro livello di persistenza è completato, possiamo salvare i dati che stiamo ricevendo dall’API. L’unica cosa che dobbiamo fare è una piccola conversione tra i nostri modelli, dato che il modello che riceviamo nell’endpoint è diverso da quello che usiamo nel database. Creiamo due funzioni di estensione per convertire questi modelli, convertToDBModel e convertToOverviewTransactionModel.

Prima in TransactionModel, convertire il modello dell’endpoint in uno del database, 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
)

E per la panoramica, qualcosa di simile. Ma questa volta il modello di database TransactionDBModel viene trasformato in un modello di endpoint, 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, ora possiamo convertire i dati tra i nostri endpoint e il database. Aggiorniamo il nostro controller per fare queste chiamate.

Nel costruttore, includeremo la nostra classe TransferRepository. Ricorda, Spring è intelligente, quando Spring vede che abbiamo questa dipendenza nel nostro controller, Spring inietterà l’implementazione creata per TransactionRepository (basata su 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() }
    }

}

Puoi vedere nell’esempio sopra che la nostra classe repository sta usando i metodi generici di CrudRepository per salvare e trovare tutti le transazioni nel database. Usiamo anche i metodi di estensione che abbiamo creato per convertire il modello a quello più appropriato.

A questo punto, i nostri endpoint sono tutti completati. Ricompilare ed eseguire l’applicazione. Creiamo una nuova richiesta di trasferimento. Ricorda che ora dobbiamo includere tutti i dati necessari su Postman per il nostro endpoint affinché possa funzionare correttamente. Includi un corpo JSON come l’immagine sotto e invia.

Schermata Postman

Dopo l’invio, tutto dovrebbe funzionare correttamente. Dovresti ricevere uno status 200, il che significa che la richiesta è stata un successo. Ora se recuperi di nuovo tutti i dati, vedrai i dati di trasferimento inviati, inclusi la data e l’id generato.

Schermata Postman

La nostra API sta funzionando 🎉 ma non abbiamo ancora finito!

Database Console

Esiste un modo molto utile che H2 Database ci offre per controllare i dati salvati ed è la “H2 Console”. È un’interfaccia web amministrativa dove puoi controllare i dati del database ed eseguire query.

Prima di tutto, per abilitare questa console devi aggiungere il seguente codice nel tuo file application.properties all’interno della cartella resource.

# Enabling H2 Console
spring.h2.console.enabled=true

Dopo aver aggiunto, esegui di nuovo la tua applicazione e inserisci nel tuo browser l’URL http://localhost:8080/h2-console. Vedrai una finestra di login per connetterti al tuo database. L’unica cosa che devi cambiare è il JDBC Url. Controlla i tuoi log della console e cerca “H2 console available at …” come nella figura seguente.

Copia l’URL del database e premi connect. Dovresti vedere la console. Ora puoi giocare con il tuo database, eseguire query SQL e vedere i dati salvati dalla tua API.

Console logs

Ora abbiamo finito, giusto? SBAGLIATO! Siamo buoni sviluppatori e i buoni sviluppatori scrivono test!

Test unitari

Prima di scrivere il nostro primo test, aggiungiamo una nuova dipendenza nel blocco delle dipendenze all’interno del build.gradle.kts. Con questa dipendenza, invece di Mockito, useremo Mockk per creare istanze mock.

// ... other dependencies already included
testImplementation("com.ninja-squad:springmockk:3.0.1")

Questa libreria ci aiuta a supportare meglio Mockk nel contesto di Spring. Puoi leggere di più a riguardo qui.

Crea una nuova classe chiamata PaymentsControllerTest all’interno della cartella tests. Prenditi un momento per dare un’occhiata ai test sottostanti e capiremo cosa sta succedendo.

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’idea generale di ciò che i test stanno facendo è facile da capire grazie al framework di test di Spring, che è molto espressivo. Diamo un’occhiata alle tecnicalità:

  • @WebMvcTest: Questa annotazione indica che questi test stanno testando un componente MVC, in questo caso, un controller REST.
  • MockMVC: Questo è il punto di ingresso che ci permetterà di comunicare con il nostro controller durante i test, effettuando richieste e verificando i risultati e gli stati.
  • @MockkBean: Stiamo facendo il mock di una classe TransferRepository finta per simulare diversi scenari. Simuliamo questi scenari sempre prima di eseguire un’azione con MockMvc, facendo il mock del risultato dei metodi findAll e save.

Dopo aver configurato i nostri mock, eseguiamo una richiesta al controller utilizzando MockMvc. Tutti i test seguono una struttura simile:

  • Diciamo a MockMvc di eseguire una richiesta e che tipo di richiesta è (post o get).
  • Se abbiamo un corpo nella richiesta, trasformiamo l’oggetto in JSON.
  • Il tipo di contenuto della richiesta, APPLICATION_JSON.
  • Stampiamo nella console i dettagli della richiesta e i risultati.
  • E infine, dichiariamo cosa ci aspettiamo da questa richiesta. Può essere un codice di stato per verificare se la richiesta ha successo o una risposta JSON (e il contenuto).

E abbiamo finalmente finito. 🎉🎉🎉

Puoi trovare il progetto completo su GitHub.


Domande o suggerimenti, per favore lascia un commento, siamo tutti qui per imparare insieme. 🤠

Grazie per aver letto.

comments powered by Disqus

Post correlati