API first dev + Service + JUnitTest: riepilogo

Semplificazione di questo articolo : API-first development con Swagger+ Spring

Passaggi:

  1. Design delle API con Swagger Editor  ovvero creo un file YAML
  2. Creo progetto Spring da Initializr vedi es. configurazione da questo articolo
  3. Importo progetto su IDE es. Eclipse
  4. Nel pom.xml inserisco dependencies + OpenApi Maven Plugin (vedi pom.xml esempio da github di reflectoring.io) dependency e plugin: https://pastebin.com/KaA1vuim 
  5. Creazione della classe Service che implementa la classe delegate
  6. Override dei metodi della classe delegate nella classe Service
  7. Su Insomnia creazione request API get e post per testare i vari endpoint
  8. creazione classe TEST
  • Nel service ci va la logica di business + responseEntity httpStatus 
  • volendo si può dividere la logica in un’altra classe utility
  • Nel test api testiamo solo la risposta e il type
  • Nel test logic testiamo la logica

Design delle API con swagger editor:

openapi: 3.0.2
info:
  title: Match organiser
  description: This is a server to organize and play matches of tic tac toe or connect
    four with some bots
  termsOfService: http://swagger.io/terms/
  contact:
    email: test@senseisrl.it
  license:
    name: Apache 2.0
    url: http://www.apache.org/licenses/LICENSE-2.0.html
  version: 1.0.0
externalDocs:
  description: Find out more about Swagger
  url: http://swagger.io
servers:
- url: https://localhost:8080/games
- url: http://localhost:8080/games
tags:
- name: connect-four
  description: Everything you can do connect-four related
paths:

  /connect-four/matches:
    get:
      tags:
      - connect-four
      summary: get the list of all the available matches
      operationId: findAllConnectFourMatches
      responses:
        200:
          description: successful operation
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ConnectFourMatch'
        404:
          description: matches not found
          content:
            text/plain; charset=utf-8:
              schema:
                type: string
        500:
          description: not implemented
          content:
            text/plain; charset=utf-8:
              schema:
                type: string
          
    post:
      tags:
      - connect-four
      summary: create a new connect four match
      operationId: createNewConnectFourMatch
      requestBody:
          description: description
          content:
            application/json:
              schema:
                items:
                  $ref: '#/components/schemas/ConnectFourMatch'
          required: true
      responses:
        200:
          description: successful operation
          content:
            application/json:
              schema:
                items:
                  $ref: '#/components/schemas/ConnectFourMatch'
        404:
          description: matches not found
          content:
            text/plain; charset=utf-8:
              schema:
                type: string
        500:
          description: not implemented
          content:
            text/plain; charset=utf-8:
              schema:
                type: string
components:
    ConnectFourMatch:
      type: object
      properties:
        id:
          type: integer
          format: int32
        playerName:
          type: string
        row:
          type: integer
          format: int32
        col:
          type: integer
          format: int32
        connectedElements:
          type: integer
          format: int32

Dopo aver fatto i punto dal 2 al 4, maven OpenAPI plugin dovrebbe avere generato in automatico delle classi suddivise nelle cartella api :

  • ApiUtil.java, ConnectFourApi.java, ConnectFourApiController.java, ConnectFourApiDelegate.java

e nella cartella model:

  • ConnectFourMatch

I model sarebbero le nostre entity, mentre la apidelegate include tutti le api post, get etc che abbiamo creato nello swagger.

Ordiniamo tutto in packages cosi:

 

Creare classe Service che implementa la classe ApiDelegate

Ora dobbiamo per prima cosa creare una classe che annoteremo come @Service e che implementerà l’interfaccia ConnectFourApiDelegate.java

 

import org.springframework.stereotype.Service;

@Service
public class ConnectFourApiDelegateSERVICE implements ConnectFourApiDelegate {
	

}

Se diamo un’occhiata alla classe ConnectFourApiDelegate
troviamo due metodi che abbiamo impostato nello swagger: una GET e una POST. Entrambi i metodi ritornano una risposta di default return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); cosi anche se noi non facciamo alcuna implementazione otteniamo un messaggio di risposta quando andiamo a chiamare l’endpoint da un tool per testare le API come ad esempio Insomnia o Postman.

  • metodo findAllConnectFourMatches che è una GET e ritorna una lista di array di content json
  • metodo createNewConnectFourMatch che è una POST e ritorna un content json
     
/**
     * GET /connect-four/matches : get the list of all the available matches
     *
     * @return successful operation (status code 200)
     *         or Invalid input (status code 400)
     *         or matches not found (status code 404)
     *         or not implemented (status code 500)
     * @see ConnectFourApi#findAllConnectFourMatches
     */
    default ResponseEntity<List> findAllConnectFourMatches() {
        getRequest().ifPresent(request -> {
            for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
                if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                    String exampleString = "{ \"col\" : 1, \"playerName\" : \"playerName\", \"connectedElements\" : 5, \"id\" : 0, \"row\" : 6 }";
                    ApiUtil.setExampleResponse(request, "application/json", exampleString);
                    break;
                }
            }
        });
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

Ora possiamo fare Override dei metodi delle post e delle get e scrivere la nostra business Logic. iniziamo col metodo che restituisce la lista dei match disponibili.

@Service
public class ConnectFourApiDelegateSERVICE implements ConnectFourApiDelegate {
	
	
	private List matchList = Collections.synchronizedList(new ArrayList<>());

	@Override
	public ResponseEntity<List> findAllConnectFourMatches() {
 		return new ResponseEntity<>(matchList, HttpStatus.OK);
	}

}

 

Junit Test:

Ora possiamo fare il più semplice dei test, l’Api test della GET :

 

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.MethodMode;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;


@SpringBootTest
@AutoConfigureMockMvc
@TestInstance(Lifecycle.PER_CLASS)
class ConnectFourApiDelegateSERVICETest {

	@Autowired
	private MockMvc mvc;
	
	/*
	 * GET - Find All Connect Four matches
	 */
	@DirtiesContext(methodMode = MethodMode.AFTER_METHOD)
	@Test
	void testFindAllConnectFourMatches() throws Exception {

		mvc.perform(MockMvcRequestBuilders.get("/games/connect-four/matches"))
				.andExpect(status().isOk())
				.andExpect(content().contentType(MediaType.APPLICATION_JSON));
	}

}

l’annotazione: @DirtiesContext(methodMode = MethodMode.AFTER_METHOD) serve a …
l’annotazione:
@AutoConfigureMockMvc
@TestInstance(Lifecycle.PER_CLASS)
serve a …

Testiamo le API con Insomnia:

Su insomnia creiamo una nuova request di test di tipo GET con url: http://localhost:8080/games/connect-four/matches

se la facciamo startare dovremmo avere una response di tipo 200 OK e un array vuoto di ritorno.

 

Implementiamo la logica di business nella classe ConnectFourApiDelegateSERVICE

Facciamo una cosa breve di esempio per capire il giro. lavoriamo sul metodo createNewConnectFourMatch facendo override nella classe Service

      • metodo createNewConnectFourMatch che troviamo nella classe ConnectFourApiDelegate che ha generato il plugin maven OpenAPI in automatico.
      • andiamo nella classe che implementa la ConnectFourApiDelegate e  che abbiamo annotato come @Service
      • facciamo override del metodo createNewConnectFourMatch della request POST in questione
      • inseriamo logica di business nel metodo
      • su Insomnia creiamo i test delle API a quell’endpoint al quale punta la POST di createNewConnectFourMatch /connect-four/matches

 classe ConnectFourApiDelegate :

    /**
     * POST /connect-four/matches : create a new connect four match
     *
     * @param connectFourMatch description (required)
     * @return successful operation (status code 200)
     *         or Invalid input (status code 400)
     *         or matches not found (status code 404)
     *         or not implemented (status code 500)
     * @see ConnectFourApi#createNewConnectFourMatch
     */
    default ResponseEntity createNewConnectFourMatch(ConnectFourMatch connectFourMatch) {
        getRequest().ifPresent(request -> {
            for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
                if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                    String exampleString = "{ \"col\" : 1, \"playerName\" : \"playerName\", \"connectedElements\" : 5, \"id\" : 0, \"row\" : 6 }";
                    ApiUtil.setExampleResponse(request, "application/json", exampleString);
                    break;
                }
            }
        });
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

 

 

 classe ConnectFourApiDelegateSERVICE :

Facciamo override del metodo createNewConnectFourMatch della request POST

	@Override
	public ResponseEntity createNewConnectFourMatch(ConnectFourMatch connectFourMatch) {

		// TODO Inseriamo la logica di business di creazione del match		
		return new ResponseEntity<>(connectFourMatch, HttpStatus.OK);

	}
  • dobbiamo fare implement della classe ConnectFourMatch , praticamente la classe ConnectFourMatch è solo quella di trasporto dall’API al nostro codice di backend, infatti nello swagger mettiamo gli schemas solo con le cose minime indispensabili. Facendo invece l’implementazione dobbiamo immaginare di creare le entità proprio del progetto. Praticamente usiamo il match “di trasporto” per ottenere i parametri forniti dall’utente ma poi li trasferiamo sull’entità per fare la businesslogic e poi rimandiamo indietro però sempre il match di trasporto.

 

Esempio:Obiettivo restituire una stringa in Insomnia che rappresenti la board del match
Endpoint /connect-four/matches/{matchId}/hr:

  •  nell’editor swagger impostare le caratteristiche di questo endpoint: parametri, operationId, request, response, in questo caso diciamo che richiediamo un parametro id del match per capire di quale match vogliamo la rappresentazione, più indichiamo la risposta in text/plain; charset=utf-8: String
  •  una volta compilato il plugin maven openapi e generato il delegate andiamo a fare override nella classe Service, del metodo indicato nello yaml con operationId: getConnectFourRepresentation
  • Siccome ogni match ha una sua board, mettiamola come attributo nella classe del ConnectFourMatchENT, la inzializziamo con i valori row e col che riceviamo dal client  e creiamo anche  un metodo legato al match per stampare la board.
  • Nel Service quando andrò a implementare il metodo che richiede ricordiamo come parametro il matchId per risalire al match del quale va stampata la board, 1. prima dovrò cercare il match tramite id 2.  potrò richiamare da quel match il metodo creato prima, printBoard.
  /connect-four/matches/{matchId}/hr:
    get:
      tags:
      - connect-four
      summary: get a visual representation of the current match
      operationId: getConnectFourRepresentation
      parameters:
      - name: matchId
        in: path
        description: ID of the match that is to be represented
        required: true
        schema:
          type: integer
          format: int32
      responses:
        200:
          description: successful operation
          content:
            text/plain; charset=utf-8:
              schema:
                type: string
        400:
          description: Invalid input
          content:
            text/plain; charset=utf-8:
              schema:
                type: string
        404:
          description: match not found
          content:
            text/plain; charset=utf-8:
              schema:
                type: string
        500:
          description: not implemented
          content:
            text/plain; charset=utf-8:
              schema:
                type: string

 

genero lo yaml e le classi con mavn openapi plugin
nel delegate:

    /**
     * GET /connect-four/matches/{matchId}/hr : get a human representation of the current match
     *
     * @param matchId ID of the match that is to be represented (required)
     * @return successful operation (status code 200)
     *         or Invalid input (status code 400)
     *         or match not found (status code 404)
     * @see ConnectFourApi#getConnectFourRepresentation
     */
    default ResponseEntity getConnectFourRepresentation(Integer matchId) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

Siccome ogni match ha una sua board:

  • mettiamo l’attributo board nella classe del ConnectFourMatchENT int[][] board e lo facciamo inizializzare (in questo caso direttamente nel costruttore del match perché mi è più comodo) grazie ai parametri Integer row and col che riceviamo dal client tramite insomnia, quando otteniamo  il Json che è un oggetto ConnectFourMatch di “trasporto” .
  • Creiamo anche un metodo per stampare la board ciclando i row prima e dentro le col e aggiungendo con append, un metodo della classe StringBuilder, alla stringa, i risultati del ciclo.

 

public class ConnectFourMatchENT extends ConnectFourMatch {
        //altri attributi omessi per brevità di codice
	private int[][] board;

	public ConnectFourMatchENT(Integer row, Integer col) {
		super();
		this.board = new int[row][col];
	}

//metodo per stampare la board 

	public StringBuilder printBoard(int[][] board) {

		StringBuilder res = new StringBuilder();

		for (int i = 0; i < board.length; i++) {

			for (int j = 0; j < board[i].length; j++) {
				//System.out.print("|" + board[i][j] + "|");
				res.append("|" + board[i][j] + "|");
	
			}//System.out.println();
			res.append("\n");
		}
		return res;

	}

nella classe implementata del delegate Service:


	public ConnectFourMatchENT findMatchENTById(Integer matchId) {

		for (ConnectFourMatchENT m : matchENTList) {

			if (m != null && m.getId().equals(matchId)) {

				return m;
			}

		}
		return null;

	}

	@Override
	public ResponseEntity getConnectFourRepresentation(Integer matchId) {

		ConnectFourMatchENT m = findMatchENTById(matchId);

		StringBuilder res = m.printBoard(m.getBoard());

		return (m != null) ? new ResponseEntity<>(res.toString(), HttpStatus.OK)
				: new ResponseEntity<>(HttpStatus.BAD_REQUEST);

	}
  • Creo metodo findMatchById
  • richiamo dentro getConnectFourRepresentation il metodo del match: printBoard
  • Ora se chiamiamo l’endpoint /tic-tac-toe/matches/{matchId}/hr
Il nostro voto
Clicca per votare questo articolo!
[Voti: 0 Media: 0]

Lascia un commento