UNIBO
Department of Computer Science

Relazione progetto MaraffaOnline - SAP

 
Matteo Santoro
Supervisor: Alessandro ricci

2026-02-17

Declaration

I, Matteo Santoro, del Dipartimento di Informatica dell’Università di Bologna, confermo che il presente lavoro è di mia proprietà e che le figure, le tabelle, le equazioni, i frammenti di codice, le opere d’arte e le illustrazioni contenute in questa relazione sono originali e non sono state tratte da lavori di altre persone, tranne nei casi in cui i lavori di altri sono stati esplicitamente riconosciuti, citati e referenziati. Sono consapevole che, in caso contrario, si configurerà un caso di plagio. Il plagio è una forma di cattiva condotta accademica e sarà sanzionato di conseguenza.
 

Matteo Santoro

2026-02-17

List of Abbreviations

School of Mathematical, Physical and Computational Sciences

Introduction

MaraffaOnline è un’applicazione che permette alle persone di giocare al gioco di carte Maraffa/Beccacino. Il progetto consiste nell’eseguire una manutenzione evolutiva del gioco di carte reperibile su MaraffaOnline, attualmente sviluppato dalla prof.ssa Lumini. In particolare la nuova versione avrà un’architettura a microservizi e introdurrà anche nuove funzionalità come formazione personalizzata delle squadre, una nuova modalità di gioco (vittoria 11 a 0 in caso di violazione delle regole da parte di una squadra), salvataggio delle statistiche delle partite e degli utenti, ...
Per lo sviluppo è stato seguito un approccio Domain Driven Design, per il quale si è approfondito il dominio del gioco.
È stata posta particolare attenzione alle tecniche di continuos integration, alle quali è stato dedicato uno dei capitoli di questo report.

Decomposizione da monolite a micro-servizi (Assignment 5)

La progettazione dell’architettura microservices di MaraffaOnline segue il metodo iterativo, articolato in due passi principali: l’identificazione delle system operations e la decomposizione in servizi tramite l’approccio Domain-Driven Design (DDD).

Passo 1 – Identificazione delle System Operations

Domain Model di Alto Livello

Il primo passo consiste nel costruire un domain model di alto livello, derivato dai sostantivi delle user stories principali. Questo modello definisce il vocabolario per descrivere le system operations.

Le classi chiave identificate per MaraffaOnline sono:

  • Player – Giocatore registrato nella piattaforma

  • User – Profilo utente con credenziali e statistiche

  • Game – Partita di Maraffa (4 giocatori, 2 squadre)

  • Team – Coppia di giocatori alleati

  • Card – Carta da gioco del mazzo

  • Hand – Mano di carte di un giocatore

  • Trick – Singola presa (4 carte giocate)

  • Round – Singola mano di gioco (distribuzione carte fino a esaurimento)

  • ChatMessage – Messaggio nella chat di gioco

Si noti come il termine “Player” e “User” rappresentino lo stesso individuo ma in contesti diversi: User nel contesto di autenticazione e gestione profilo, Player nel contesto della logica di gioco. Questa polisemia è un segnale naturale dell’esistenza di bounded context distinti, dove lo stesso concetto del mondo reale assume significati e attributi diversi in ciascun contesto.

Casi d’uso

Si riporta di seguito lo schema dei casi d’uso che modella l’interazione dell’utente con l’applicazione.

Schema dei casi d’uso

Event Storming

Per raffinare il domain model e identificare le operazioni chiave, è stato utilizzato l’Event Storming. L’analisi ha prodotto i seguenti elementi:

image

Event Storming

Domain Events:

  • UserRegistered, UserLoggedIn, UserLoggedOut

  • GameCreated, PlayerJoined, GameStarted

  • CardsDealt, CardPlayed, TrickWon

  • MaraffaDeclared, RoundEnded, GameEnded

  • MessageSent

Commands:

  • RegisterUser, Login, Logout

  • CreateGame, JoinGame, StartGame

  • DealCards, PlayCard, DeclareMaraffa

  • SendMessage

System Operations

Dall’analisi degli eventi e dei comandi si derivano le system operations, suddivise in commands (operazioni che modificano lo stato) e queries (operazioni di sola lettura):

Commands:

  • createUser(username, password, email) – Registra un nuovo utente

  • login(username, password) token – Autentica l’utente

  • createGame(playerId) gameId – Crea una nuova partita

  • joinGame(playerId, gameId) – Un giocatore entra nella partita

  • playCard(playerId, gameId, card) – Gioca una carta nel turno corrente

  • sendMessage(playerId, gameId, text) – Invia un messaggio in chat

Queries:

  • getGameState(gameId) – Stato corrente della partita

  • getUserProfile(userId) – Profilo e statistiche utente

  • getUserStats(userId) – Statistiche di gioco dell’utente

  • getActiveGames() – Lista partite disponibili

Passo 2 – Decomposizione in Servizi

Identificazione dei Subdomains

Seguendo l’approccio DDD, il primo passo della decomposizione consiste nell’analizzare il dominio di business per identificare i subdomains, ossia le diverse aree di competenza dell’applicazione. I subdomains vengono scoperti attraverso l’analisi del business, non progettati.

Per MaraffaOnline sono stati identificati tre subdomains:

  1. Game Logic (Core Subdomain) – Contiene le regole della Maraffa, la gestione delle partite, il calcolo dei punteggi e la logica dei turni. È il subdomain core perché rappresenta ciò che l’applicazione fa diversamente da qualsiasi altra piattaforma: implementare correttamente le regole specifiche della Maraffa, un gioco di carte regionale con regole non banali.

  2. User Management (Generic Subdomain) – Comprende registrazione, autenticazione, gestione profili e statistiche. È un subdomain generic perché l’autenticazione e la gestione utenti sono attività che tutte le applicazioni svolgono allo stesso modo.

  3. Chat (Supporting Subdomain) – Gestisce la messaggistica in tempo reale durante le partite. È un subdomain supporting perché supporta l’esperienza di gioco ma non fornisce nessuna funzionalità fondamentale.

Progettazione dei Bounded Contexts

Per MaraffaOnline sono stati progettati due bounded contexts principali, con una corrispondenza quasi uno-a-uno con i subdomains:

  1. Game Context – Corrisponde al core subdomain Game Logic. Il domain model include le entità Game, Player, Round, Card, Team, Trick. In questo contesto, “Player” indica un partecipante attivo nella partita, con attributi come la mano di carte, il team di appartenenza e il turno corrente. L’ubiquitous language include termini specifici del gioco: mano, presa, briscola, Maraffa.

  2. User Context – Corrisponde al generic subdomain User Management. Il domain model include User, Credentials, UserStats. In questo contesto, “User” indica un profilo registrato con credenziali, email e storico partite. L’ubiquitous language è quello standard della gestione utenti: registrazione, login, profilo.

Il subdomain Chat non è stato implementato come bounded context autonomo: la funzionalità di messaggistica è stata integrata nel middleware, una scelta discussa nella Sezione 2.2.5.

Context Map

La context map rappresenta visivamente le relazioni e i pattern di integrazione tra i bounded contexts del sistema:

  • Game Context ↔︎ User Context: relazione di tipo Customer-Supplier. Il Game Context ha bisogno di verificare l’identità dei giocatori e aggiornare le statistiche. Lo User Context (supplier) fornisce il servizio di autenticazione e l’accesso ai profili. Il Game Context si conforma al contratto esposto dallo User Context per l’autenticazione, accettando il formato del token JWT così com’è.

  • Middleware ↔︎ Game Context: il middleware agisce come API Gateway e instrada le richieste verso il Game Context. Traducendo le richieste HTTP/WebSocket del frontend nel formato atteso dal servizio di business logic.

  • Middleware ↔︎ User Context: il middleware instrada le richieste di autenticazione e gestione profilo verso lo User Context, operando come semplice proxy, senza trasformare il modello.

Dai Bounded Contexts ai Servizi

Ogni bounded context è stato mappato a un microservizio indipendente, seguendo l’allineamento naturale tra DDD e architettura microservices:

Mappatura subdomains bounded contexts servizi
Subdomain Tipo Bounded Context Servizio
Game Logic Core Game Context business-logic (porta 3000)
User Management Generic User Context user-service (porta 3001)
Chat Supporting (nel middleware) middleware (porta 3003)

Trade-off Architetturale: il Ruolo del Middleware

Il servizio middleware svolge molteplici responsabilità che, in un’architettura microservices ideale, sarebbero separate:

  • API Gateway – Routing delle richieste HTTP verso i servizi interni

  • WebSocket Management – Gestione delle connessioni in tempo reale per gli aggiornamenti di gioco

  • Chat – Logica di messaggistica tra giocatori (subdomain supporting)

  • Orchestrazione parziale – Coordinamento tra user-service e business-logic per alcune operazioni

Questa concentrazione di responsabilità rappresenta un trade-off consapevole: in un progetto accademico con un team ridotto, separare ulteriormente il middleware avrebbe introdotto complessità operativa (più container, più configurazioni di rete, più punti di failure) senza un beneficio proporzionale. Tuttavia, è importante riconoscere che questa scelta viola parzialmente il principio di singola responsabilità a livello di servizio e potrebbe rappresentare un ostacolo alla scalabilità indipendente dei componenti in un contesto produttivo.

In una evoluzione futura, si potrebbe considerare:

  • Estrarre la chat in un servizio dedicato (chat-service)

  • Separare la gestione WebSocket in un servizio di notifiche (notification-service)

  • Mantenere il middleware come puro API Gateway

Pattern architetturali applicati

API Gateway Pattern

Il Middleware implementa il pattern API Gateway, fungendo da unico punto di ingresso per tutti i client. Questo approccio centralizza diverse funzionalità critiche:

  • Routing: smista le richieste ai microservizi corretti

  • Authentication: valida i token JWT prima di inoltrare le richieste

  • Game Orchestration: coordina la logica di gioco tra i servizi

  • WebSocket Management: gestisce le connessioni real-time per gioco e chat

Si è seguito un approccio modulare nello sviluppo di questo componente che, in un primo step, conteneva soltanto la logica di gestione delle partite, al quale sono stati aggiunti i moduli per la comunicazione con il servizio degli utenti e della business logic. Ogni modulo segue la stessa struttura, ossia una classe denominata controller che si occupa esclusivamente della dichiarazione delle rotte HTTP e che a sua volta contiene un service. Ogni servizio contiene poi le logiche di calcolo e crea un JSON di risposta che viene passato al controller che lo invia come risposta HTTP, questo per poter separare completamente e rispettare il principio di single responsibility.

Nel modulo contenente le logiche di gioco è stato necessario aggiungere un ulteriore strato di separazione alla struttura controller-service, data la struttura multi-attore della gestione delle partite che non permetteva una separazione netta. Quindi è stato aggiunto, tra il controller e il service, un componente ibrido per poter comunque avere servizi indipendenti.

Questa separazione, oltre a essere una buona pratica, è stata scelta per poter effettuare del testing comodamente soltanto sui servizi, testando quindi solamente la parte relativa all’elaborazione di una risposta.

Database per Service Pattern

È stato applicato parzialmente il pattern "Database per Service" per garantire il disaccoppiamento dei dati:

  • UserService: utilizza MySQL per dati relazionali strutturati (utenti, credenziali, statistiche)

  • Middleware: utilizza MongoDB per lo storico delle partite e dati flessibili

  • BusinessLogic: servizio completamente stateless, non necessita di database

Ogni microservizio gestisce i propri dati in modo indipendente, evitando dipendenze dirette sui database di altri servizi.

Comunicazione tra servizi

API Gateway

Sono state implementate diverse forme di comunicazione tra servizi in base alle caratteristiche del messaggio che viene scambiato. Si è scelto di utilizzare la comunicazione REST nella maggior parte dei servizi, vista la sua interoperabilità.

Non è stato necessario implementare un vero e proprio broker di messaggi, ma è stato implementato un servizio denominato middleware che svolge il ruolo di API gateway delle richieste dai client verso il sistema.

Queste chiamate non sono obbligatoriamente dirette verso il servizio interessato, ma possono anche essere routine che effettuano molteplici chiamate per ottenere il risultato desiderato. Implementare queste chiamate direttamente nel front-end avrebbe reso il codice molto più complesso e difficile da mantenere, e la gestione di partite multiple sarebbe stata molto più complicata.

Non è stato implementato nessun meccanismo di discovery per scoprire l’indirizzo degli altri servizi, è stata sfruttata la tecnologia built-in di docker che permette di poter utilizzare degli alias e nascondere al proprio interno i meccanismi di registration, questo comunque implica che tutto debba essere sempre mantenuto nell’ambiente virtualizzato e quindi ogni futuro servizio containerizzato.

WebSocket per comunicazione Real-Time

L’interazione con il frontend avviene prevalentemente tramite il protocollo HTTP, ma per la gestione di alcuni aspetti in tempo reale sono state introdotte le WebSocket.

Dashboard reattiva

In una prima implementazione era stato utilizzato un polling per aggiornare i dati delle partite. Tramite WebSocket, è direttamente il middleware che notifica tutti i client della creazione di una nuova partita via broadcast, migliorando significativamente la reattività dell’interfaccia.

Chat globale e privata

Questo componente è stato creato per la gestione delle chat di gioco, in modo che i giocatori possano comunicare tra loro in tempo reale. Il sistema implementa due tipologie di chat:

  • Chat globale: accessibile a tutti gli utenti connessi

  • Chat di partita: accessibile solo ai giocatori della partita in corso

Ogni client, istanziato sul computer degli utenti, effettua una connessione WebSocket con il middleware utilizzando come endpoint il proprio UUID, che viene assegnato automaticamente all’avvio, e lo username del giocatore, dopo aver effettuato il login. Il middleware contiene al proprio interno una mappa di tutti i client e giocatori connessi con la relativa WebSocket e una classe che espone due metodi per inviare messaggi a un singolo giocatore o effettuare un messaggio broadcast a tutti i giocatori connessi.

Il middleware si occupa di instradare i messaggi alle giuste chat, garantendo la privacy delle conversazioni di partita.

Eventi di gioco

Il modulo di game si occupa di gestire le partite, quindi è l’unico che può sapere quali giocatori devono ricevere determinati messaggi relativi a una singola partita. Per utilizzare il servizio di WebSocket, è stata implementata una semplice interfaccia all’interno dell’attore di gioco con la quale si effettuano chiamate "onEvent". Ogni volta che un particolare evento si verifica, un’invocazione a queste chiamate permette di inviare messaggi alle WebSocket dei giocatori interessati.

L’interfaccia definisce i principali eventi del ciclo di vita di una partita:

public interface IGameActor {
    void onCreateGame(User user);
    void onNewRound();
    void onJoinGame(User user);
    void onStartGame();
    void onPlayCard();
    void onTrickCompleted(Trick latestTrick);
    void onEndRound();
    void onEndGame();
    // ... altri eventi
}

Deployment e DevOps

Containerizzazione

Ogni servizio è stato dockerizzato utilizzando multi-stage builds per ottenere immagini leggere e ottimizzate. Questa tecnica permette di separare la fase di build dalla fase di esecuzione, includendo nell’immagine finale solo i file strettamente necessari.

Per i servizi Node.js (UserService e BusinessLogic), il primo stage utilizza un’immagine completa per la compilazione, mentre il secondo stage copia solo i file compilati e le dipendenze di produzione.

Il middleware Java utilizza un’immagine Gradle per la build e OpenJDK per l’esecuzione, producendo un Fat JAR che include tutte le dipendenze necessarie.

Il frontend Angular viene compilato in un primo stage e poi servito tramite Nginx, che gestisce anche il reverse proxy verso i servizi backend.

Stack Docker e reti

Il sistema utilizza reti Docker interne per la comunicazione tra container, con IP privati assegnati automaticamente. I servizi non sono esposti direttamente all’esterno, creando un’architettura più sicura. Gli indirizzi sono configurati tramite variabili d’ambiente, facilitando la gestione di diversi ambienti di deployment.

Struttura repository

Il progetto utilizza repository separate per ogni microservizio, collegate tramite git submodules. Questa scelta garantisce:

  • Indipendenza nello sviluppo di ogni componente

  • Possibilità di utilizzare workflow CI/CD specifici per ogni servizio

  • Versionamento indipendente dei servizi

La repository principale contiene i file docker-compose per l’orchestrazione dei container e i submodules che puntano alle singole repository dei servizi.

Continuous Integration

Ogni repository ha workflow automatizzati tramite GitHub Actions che includono:

  • Build: compilazione del codice

  • Test: esecuzione dei test automatici

  • Deploy: build e push dell’immagine Docker

I workflow vengono attivati su pull request verso il branch develop e su release manuali sul branch main. Il progetto adotta Conventional Commits per standardizzare i messaggi di commit e facilitare la generazione automatica di changelog.

Monitoraggio

È stato implementato uno stack completo di monitoraggio basato su:

  • cAdvisor: raccolta metriche dei container (CPU, memoria, rete, disco)

  • Prometheus: storage delle metriche time-series e sistema di alerting

  • Grafana: dashboard per la visualizzazione e analisi delle performance

Questo sistema permette di monitorare in tempo reale lo stato di salute dei servizi e identificare eventuali colli di bottiglia o anomalie.

Documentazione e testing

Swagger / OpenAPI

Per la documentazione delle API è stato utilizzato Swagger, che genera automaticamente la documentazione interattiva:

  • Per i servizi Node.js: libreria che genera documentazione tramite decoratori

  • Per i servizi Java: documentazione generata tramite annotazioni

Postman e testing

È stato utilizzato Postman per il testing delle API, con:

  • Collection di richieste esportate da Swagger

  • Payload parametrizzati con variabili d’ambiente

  • Flows: diagrammi di flusso delle richieste per simulare partite complete end-to-end

Sistema di Logging

Il middleware implementa un sistema di log automatici tramite LogBack che permette:

  • Log su file per analisi in caso di problemi

  • Configurazione XML per personalizzazione (package, livelli, log rotate)

  • Compressione automatica dei log oltre una certa dimensione

Vantaggi e sfide

Vantaggi dell’architettura

Fault Tolerance

La scelta dell’architettura a microservizi permette di isolare parti del sistema, evitando che gli errori si propaghino. Ogni servizio opera in modo indipendente con le proprie risorse. Tuttavia, il middleware rappresenta un Single Point of Failure che potrebbe essere mitigato implementando backup dei processi di gestione partite o ridondanza con istanze multiple.

Scalabilità

Ogni microservizio può scalare in modo indipendente in base al carico. Utilizzando orchestratori come Kubernetes è possibile bilanciare il carico tra nodi e scalare orizzontalmente aggiungendo repliche dei servizi sotto maggior stress.

Indipendenza tecnologica

L’utilizzo di due stack tecnologici differenti (Java/Vert.x per il middleware e NestJS per gli altri servizi) dimostra la flessibilità dell’architettura e permette di scegliere la tecnologia più adatta per ogni componente.

Sfide incontrate

Durante lo sviluppo sono emerse alcune criticità:

  • Complessità nella gestione della comunicazione tra servizi

  • Necessità di coordinamento per la definizione delle interfacce API

  • Overhead di deployment con tecnologie multiple

  • Il middleware come single point of failure richiede attenzione particolare

Conclusioni

La decomposizione del monolite MaraffaOnline in un’architettura a microservizi ha permesso di ottenere significativi vantaggi in termini di manutenibilità, scalabilità e fault isolation.

I pattern applicati con successo includono:

  • API Gateway per centralizzare l’accesso ai servizi

  • Database per Service per il disaccoppiamento dei dati

  • Decompose by Subdomain per definire confini chiari seguendo DDD

  • WebSocket per la comunicazione real-time

Il sistema risultante è facilmente deployabile tramite Docker, monitorabile attraverso strumenti standard (Prometheus/Grafana) e pronto per essere scalato in base alle necessità. La separazione in microservizi indipendenti facilita inoltre la manutenzione evolutiva e l’aggiunta di nuove funzionalità senza impattare l’intero sistema.

Quality Attributes

In questa sezione vengono analizzati gli attributi di qualità (quality attributes) rilevanti per l’architettura del sistema sviluppato. Si tratta di requisiti non funzionali che influenzano profondamente le decisioni progettuali e architetturali.

Operational

  • Robustezza: l’interfaccia limita abbastana le operazione che possono esserer effettuate, rendendo il sistema più robusto.

  • Availability/Scalabilità: il sistema è progettato per essere sempre disponibile, la replica di servizi mirati sul nodo swarm garantisce che il sistema possa continuare a funzionare anche in caso di guasti e bilanciarsi.

  • Usabilità: l’interfaccia è progettata per essere intuitiva e facile da usare, con un design che facilita l’interazione dell’utente. Non è stato previsto nel progetto la creazione di un tutorial o di una guida, ma si è cercato di rendere l’interfaccia il più semplice possibile. (Conoscere le regole del gioco di carte è stato considerato sufficiente per l’utilizzo del sistema).

Structural

  • Evolvabilità: il sistema è progettato per essere facilmente estendibile e modificabile, con un’architettura modulare che consente l’aggiunta di nuove funzionalità senza impattare le parti esistenti.

  • Portabilità: il sistema è progettato per essere facilmente distribuito su diverse piattaforme e ambienti, grazie all’uso di tecnologie containerizzate come Docker..

Testing

Per ogni servizio del sistema è stato adottato, ove possibile, il paradigma di programmazione TDD (Test Driven Development). Questo approccio prevede che i test vengano scritti prima del codice, in modo da guidarne lo sviluppo e garantire che il software prodotto soddisfi i requisiti specificati.

La strategia di testing adottata si ispira alla test pyramid descritta in , che prevede una distribuzione dei test su livelli crescenti di granularità: molti unit test alla base, un numero inferiore di integration test, e pochi test ai livelli superiori.

Strumenti di Testing

Le operazioni di testing sono state implementate con Jest per quanto riguarda i servizi basati su Node.js, generando report chiari e di facile lettura. Il componente middleware, scritto in Java, produceva invece risultati di test meno comprensibili. Per risolvere questo problema è stata introdotta, nel file di build Gradle, la libreria Test Logger, tramite la seguente configurazione:

import com.adarshr.gradle.testlogger.TestLoggerExtension
import com.adarshr.gradle.testlogger.TestLoggerPlugin
import com.adarshr.gradle.testlogger.theme.ThemeType

testlogger {
    theme = ThemeType.MOCHA
    showExceptions = true
    showStackTraces = true
    showFullStackTraces = false
    showCauses = true
    slowThreshold = 2000
    showSummary = true
    showSimpleNames = false
    showPassed = true
    showSkipped = true
    showFailed = true
    showOnlySlow = false
    showStandardStreams = false
    showPassedStandardStreams = true
    showSkippedStandardStreams = true
    showFailedStandardStreams = true
    logLevel = LogLevel.LIFECYCLE
}

Architettura dei Componenti e Strategia di Testing

L’architettura interna di ciascun servizio segue una struttura a due livelli:

  • Classi controller: espongono le rotte e delegano alle classi di servizio.

  • Classi di servizio: contengono la logica di business.

Questa separazione consente di applicare strategie di testing differenziate per ciascun livello, coerentemente con quanto suggerito dall’architettura esagonale: le classi di servizio vengono testate con solitary unit test che ne verificano la logica in isolamento, mentre le interazioni tra servizi e con l’infrastruttura vengono verificate ai livelli superiori della piramide.

Unit Test

Gli unit test costituiscono la base della test pyramid e sono orientati alla tecnologia. Nel nostro sistema, sono stati implementati come solitary unit test sulle classi di servizio, verificando la correttezza della logica di business in isolamento dalle dipendenze esterne.

Non sono stati realizzati unit test dedicati per le classi controller dei singoli servizi. I controller andrebbero testati con solitary unit test che mockano i servizi sottostanti; nel nostro caso, si è scelto di verificarne il comportamento ai livelli superiori della piramide, tramite i test descritti nella Sezione 4.5. Si tratta di una limitazione consapevole, motivata dalla semplicità dei controller che si limitano a delegare alle classi di servizio.

Integration Test

Gli integration test si collocano al livello immediatamente superiore agli unit test nella piramide. Il loro scopo è verificare che un servizio possa interagire correttamente con i servizi infrastrutturali da cui dipende, in particolare i database .

Nel nostro sistema, ad eccezione del servizio contenente la business logic — che non ha dipendenze verso database né verso altri servizi — sono stati realizzati persistence integration test per verificare il corretto collegamento tra i servizi e i rispettivi database (MySQL e MongoDB).

L’esempio seguente mostra un persistence integration test che verifica l’interazione tra il servizio di gioco e MongoDB:

@TestInstance(Lifecycle.PER_CLASS)
@ExtendWith(VertxExtension.class)
public class StatisticMongoTest {
    private static final int TRICKS = 10;
    private final User userTest = new User("user", UUID.randomUUID(), false);
    private static final int MARAFFA_PLAYERS = 4;
    private static final int EXPECTED_SCORE = 11;
    private static final String PASSWORD = "1234";
    private static final GameMode GAME_MODE = GameMode.CLASSIC;
    private Vertx vertx;
    private GameService gameService;
    private static final CardSuit UNDEFINED_TRUMP = CardSuit.NONE;
    private final Card<CardSuit, CardValue> cardTest = new Card<>(CardValue.THREE, CardSuit.CLUBS);
    private final Boolean isSuitFinished = true;
    private final MongoStatisticManager mongoStatisticManager = new MongoStatisticManager(
        Dotenv.configure().filename("env.example").load().get("MONGO_USER", "user"),
        Dotenv.configure().filename("env.example").load().get("MONGO_PASSWORD", "password"),
        Dotenv.configure().filename("env.example").load().get("MONGO_HOST", "localhost"),
        Integer.parseInt(Dotenv.configure().filename("env.example").load().get("MONGO_PORT", "27127")),
        Dotenv.configure().filename("env.example").load().get("MONGO_DATABASE", "maraffa-test")
    );

    @BeforeAll
    public void setUp() {
        this.vertx = Vertx.vertx();
        this.gameService = new GameService(this.vertx, this.mongoStatisticManager);
    }

    @AfterAll
    public void tearDown() {
        this.vertx.close();
    }

    @Test
    public void prepareGame() {
        final String gameID = this.gameService
            .createGame(MARAFFA_PLAYERS, this.userTest, EXPECTED_SCORE, GAME_MODE.toString())
            .getString(Constants.GAME_ID);
        final var doc = this.mongoStatisticManager.getRecord(gameID + "-0");
        assertNotNull(doc);
    }
}

Il test segue le fasi canoniche: setup (inizializzazione di Vert.x e del servizio con connessione a MongoDB), execute (creazione di una partita e recupero del record), verify (asserzione sulla presenza del documento), teardown (chiusura di Vert.x).

Component Test

I component test verificano il comportamento di un servizio trattandolo come una black box e interagendo con esso attraverso la sua API .

Nel nostro sistema, il componente middleware svolge il ruolo di API Gateway e si occupa di mettere in comunicazione i servizi tra loro e con il frontend. I test realizzati al suo interno verificano che le API esposte dagli altri servizi rispondano correttamente e che i contenuti vengano deserializzati nel formato atteso.

Questi test, pur non utilizzando un framework di contract testing come Spring Cloud Contract o Pact, svolgono nella pratica un ruolo analogo ai consumer-driven contract test: il middleware agisce come consumer delle API degli altri servizi, e i test verificano che il provider rispetti il contratto implicito (endpoint corretto, struttura della response attesa, status code).

Testing Asincrono

Durante la scrittura dei test del servizio Java, alcune operazioni risultavano asincrone; è stato quindi necessario attendere il completamento di tali operazioni prima di poter validare i test. La struttura di test standard fornita da Vert.x non era sufficiente in un primo momento. La forma classica dei test è la seguente:

@Test
public void createGameTest(final VertxTestContext context) {
    final JsonObject gameResponse = this.gameService.createGame(
        MARAFFA_PLAYERS, TEST_USER, EXPECTED_SCORE, GAME_MODE.toString(), PASSWORD);
    Assertions.assertEquals(UUID_SIZE, gameResponse.getString(Constants.GAME_ID).length());
    context.completeNow();
}

Per gestire correttamente le operazioni asincrone, sono stati adottati i seguenti accorgimenti:

  • utilizzo del decoratore @Timeout fornito da JUnit Jupiter, che consente di impostare un limite di tempo oltre il quale il test fallisce automaticamente, utile per gestire servizi esterni che non rispondono;

  • uso asincrono del VertxTestContext, che consente di completare il test solo quando tutte le operazioni asincrone sono terminate;

  • introduzione, nei servizi, di una struttura di risposta che include una chiave "error" in caso di fallimento, poiché il test non può accedere direttamente alla risposta HTTP e quindi allo status code.

Il seguente esempio mostra un test asincrono correttamente strutturato:

@Timeout(value = 10, unit = TimeUnit.SECONDS)
@Test
public void testGetShuffledDeckOK(final VertxTestContext context) {
    final JsonObject gameResponse = this.gameService.createGame(
        4, TEST_USER, 41, GameMode.CLASSIC.toString(), PASSWORD);
    this.businessLogicController
        .getShuffledDeck(UUID.fromString(gameResponse.getString(Constants.GAME_ID)), 4)
        .whenComplete((res, err) -> {
            context.verify(() -> {
                assertNull(res.getString("error"));
                context.completeNow();
            });
        });
}

Livelli Superiori della Piramide

I livelli superiori della test pyramid — in particolare gli end-to-end test, che dovrebbero essere minimizzati data la loro complessità, lentezza e fragilità — non sono stati implementati in forma automatizzata. La verifica del comportamento complessivo dell’applicazione è stata condotta tramite sessioni di alpha testing con più utenti attraverso il frontend, coprendo in modo esplorativo i percorsi principali dell’utente (user journey).

Micro-servizi production ready(Assignement 7)

Sicurezza

La parte di sicurezza è relativamente semplice nel progetto MaraffaOnline, viene utilizzato una form login con utente/passwword e questi vengono passati al servizio degli utente che verifica la validatà e rilascia un authorization bearer che viene salvato all’interno del browser per poter autorizzare tutte le successive chiamate ai servizi, che comunque non sono mai dirette ma vengono sempre intercettate dal servizio di API gatetway che ne controlla la validità. Dato il dominio non ci sono permessi differenti in base alle tipologie di utenti, visto che sono tutti giocatori.

Configurabilità

Il sistema è stato studiato per poter essere pienamente configurabile sfruttando le funzionalità offerte da docker e in particolare docker swarm, ogni servizio è in grado di leggere le proprie configurazioni di sistema da un file ’.env’.

Osservabilità

Health check API

In ogni servizio creato ad-hoc per il progetto sono state inserite le API per il controllo di health, forniscono un test di liveness del sistema tramite uno status code 200 e inoltre sono state aggiunte metriche di ogni servizio che potrebbero risultare utili in uno scenario di deploy in cui è importante conoscere lo spazio su disco utlizzato da ogni servizio e che percentuale occupa. All’interno dello stack, su ogni container, vengono chiamate queste API periodicamente per poter assicurare che non ci siano down di servizi.

healthcheck:
      test:
        [
          "CMD", 
          "curl", "-f", "http://localhost:3000/actuator/health", "||", 
          "curl", "-f", "http://localhost:3000/health", "||", 
          "curl", "-f", "http://localhost:3000/", "||","exit","1",
        ]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

Log aggregation

Log Tracing

Tracing servizi