SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor

Mocking e test API self-hosted su un VPS

WireMock gestisce gli stub versionati in Git, Hurl esegue le suite in CI: scopri come farli girare su un VPS e conservare i report dopo un rebuild.

Due job che condividono lo stesso repository

Il mocking e il testing delle API in self-hosting sono due attività diverse; trattarle come un’unica attività fa perdere una settimana. Un mock server sostituisce una dipendenza che non è possibile chiamare dalla CI: un provider di pagamenti, un’API di un partner, un upstream soggetto a rate limiting o un servizio che un altro team non ha ancora rilasciato. Un test runner per API chiama i propri endpoint in un ordine prestabilito e verifica le risposte, trasferendo i valori di una risposta nella richiesta successiva.

Le due attività non si sovrappongono. Un mock server non indica mai se un test è superato o fallito. Un test runner non stabilisce quale risposta debba restituire un provider di pagamenti quando una carta viene rifiutata. La maggior parte dei team che dispone già di un server finisce per eseguire un’istanza di ciascun componente, avviate dallo stesso file Docker Compose e sottoposte a revisione nella stessa pull request.

Perché eseguire in proprio il mocking e i test delle API?

I fixture contengono dati strutturati come quelli di produzione. Il body di una richiesta in un test API è un record reale di un cliente con il nome modificato, oppure con il nome non modificato perché nessuno ha verificato il dato. Gli stub registrati sono ancora più rischiosi: la registrazione tramite proxy salva ciò che il servizio upstream ha restituito realmente. Di conseguenza, una directory di stub creata tramite registrazione contiene token attivi e indirizzi email dei clienti finché qualcuno non legge ogni file. In un servizio ospitato, questi dati diventano un incidente di sicurezza a carico di terzi e una divulgazione dei tuoi dati.

Il secondo motivo è la raggiungibilità. Un servizio associato a un indirizzo privato non è raggiungibile da un runner ospitato, quindi il test non può essere eseguito. Ogni soluzione alternativa ha un costo. Pubblicare l'API su Internet per poterla testare annulla il motivo per cui era privata. Un tunnel o una copia pubblica dell'ambiente di staging costituiscono un altro sistema da gestire, e una copia di staging si discosta dalla produzione tra una release e l'altra. Un runner nella stessa rete privata chiama direttamente il servizio e non richiede nulla di tutto questo. Questo è il motivo pratico alla base di un runner self-hosted di GitHub Actions.

Quale mock server self-hosted dovresti eseguire?

Ognuno di questi strumenti viene eseguito come container su un server che controlli. La domanda importante è quale consideri la fonte autorevole dei dati, perché da questo dipende se la ricostruzione del container non comporta costi oppure richiede un intero pomeriggio.

  • WireMock conserva ogni stub come file JSON in una directory mappings/, con i corpi delle risposte di grandi dimensioni in __files/. L'immagine è wiremock/wiremock, la directory principale al suo interno è /home/wiremock e lo strumento può anche essere eseguito come proxy di registrazione. I file su disco permettono di gestire il mock in git come qualsiasi altro codice.
  • Mockoon CLI conserva un'intera API mock in un unico file di dati JSON. Installalo con npm install -g @mockoon/cli e avvialo con mockoon-cli start --data ./data-file.json, oppure esegui l'immagine mockoon/cli montando quel file tramite bind mount. L'applicazione desktop modifica lo stesso file, quindi la progettazione nell'interfaccia grafica e il commit del risultato restano compatibili.
  • MockServer viene eseguito dall'immagine mockserver/mockserver e resta in ascolto sulla porta 1080. Le aspettative vengono inviate tramite la sua API REST, una soluzione pratica dal codice dei test ma rischiosa in produzione: un'aspettativa creata con una chiamata HTTP scompare quando il container viene riavviato. Usa il suo file di inizializzazione JSON per gli stub che devono essere permanenti.
  • Prism costruisce il mock dal documento OpenAPI invece che da file di stub separati. Installalo con npm install -g @stoplight/prism-cli, quindi esegui prism mock openapi.yaml. In un container aggiungi -h 0.0.0.0, perché per impostazione predefinita Prism si associa a localhost e altrimenti non è raggiungibile dall'esterno del container.
  • Microcks è l'opzione più completa: offre un'interfaccia web che importa documenti OpenAPI e collection Postman, quindi li espone come mock ed esegue test dei contratti. Un'installazione completa richiede MongoDB e Keycloak, oltre a Kafka per le funzionalità asincrone. L'immagine microcks-uber all in one include un MongoDB in memoria, che il progetto indica come adatto all'uso effimero; considera quindi temporanei tutti gli elementi creati nell'interfaccia e conserva gli artefatti sorgente in git.

Quale runner per i test delle API self-hosted dovresti usare?

Il lavoro consiste in una sequenza: autenticarsi, creare un ordine, leggerlo nuovamente e verificare che lo stato sia cambiato. È necessario acquisire un valore da una risposta e usarlo nella richiesta successiva. Uno strumento che non può mantenere lo stato tra le chiamate esegue un controllo di disponibilità, non un test API.

  • Hurl esegue file di testo semplice contenenti richieste HTTP tramite un unico binario. Una sezione [Captures] estrae valori da una risposta, una sezione [Asserts] li verifica e --test trasforma l'esecuzione in un test runner con riepilogo e codice di uscita. La versione 8.0.1 è quella corrente ad agosto 2026.
  • Bruno CLI esegue una directory di file .bru. Installalo con npm install -g @usebruno/cli, quindi esegui bru run folder --env Local --reporter-junit results.xml. Il formato della raccolta prevede file di testo in una directory, quindi le differenze sono leggibili durante la revisione.
  • Newman esegue le raccolte Postman al di fuori di Postman: npm install -g newman, quindi newman run collection.json -r cli,junit --reporter-junit-export results.xml. Il limite è il formato. La raccolta è un unico oggetto JSON esportato, quindi la modifica avviene in Postman e il file nel repository git è una copia che diventa obsoleta.
  • Schemathesis esegue un tipo diverso di controllo. Legge uno schema OpenAPI e genera casi che provano a produrre risposte che lo schema dichiara impossibili: uvx schemathesis run https://your.api/openapi.json. Individua arresti anomali e violazioni del contratto. Non conosce le regole di business, quindi affianca una suite con test codificati invece di sostituirla.
  • Hoppscotch self-hosted è l'opzione con interfaccia Web e richiede un'istanza Postgres. Valuta questo compromesso prima di installarlo: le raccolte risiedono in un database, non nel repository.

Uno strumento da evitare è Step CI. Compare ancora nelle raccolte di strumenti e il suo formato di workflow YAML è leggibile, ma il repository ha ricevuto l'ultimo commit ad agosto 2024. Un programma che si trova tra la CI e le API è un componente inappropriato per codice non più mantenuto.

Mettere il mock server dietro il firewall

La configurazione seguente esegue WireMock come sostituto di un provider di pagamenti. Se il formato del file Compose non ti è familiare, Docker Compose su un VPS descrive i comandi per gestirne il ciclo di vita utilizzati in questa sezione.

services:
  mock-payments:
    image: wiremock/wiremock:3.13.2
    command: ["--verbose"]
    volumes:
      - ./mocks/payments:/home/wiremock
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped

Il prefisso 127.0.0.1: sulla porta è la parte importante. Un 8080:8080 senza indirizzo pubblica il mock su tutte le interfacce, compreso l'IP pubblico, e il servizio resta raggiungibile anche se ufw nega quella porta, perché Docker scrive le proprie regole nella catena DOCKER di INPUT e queste vengono valutate prima delle regole di ufw. Associa invece la porta all'indirizzo di loopback o all'indirizzo di un'interfaccia privata: in questo modo il kernel non accetta mai connessioni dall'esterno.

Il servizio sottoposto a test deve quindi puntare al mock. Se il servizio viene eseguito nello stesso progetto Compose, l'URL di base del mock è http://mock-payments:8080, perché Compose risolve i nomi dei servizi sulla propria rete. Se il servizio viene eseguito sull'host, è http://127.0.0.1:8080. Imposta questo valore tramite una variabile d'ambiente, mai nel codice, altrimenti l'URL di test finirà in produzione.

Inserisci gli stub in ./mocks/payments/mappings/, un file JSON per ciascuno.

{
  "request": {
    "method": "POST",
    "urlPath": "/v1/charges",
    "bodyPatterns": [{ "matchesJsonPath": "$.amount" }]
  },
  "response": {
    "status": 201,
    "headers": { "Content-Type": "application/json" },
    "jsonBody": { "id": "ch_test_001", "status": "succeeded", "amount": 4200 }
  }
}

Avvialo, quindi verifica che cosa è stato effettivamente caricato.

docker compose up -d --wait mock-payments
curl -fsS http://127.0.0.1:8080/__admin/mappings

--wait attende che il container risulti in stato healthy. Funziona perché l'immagine WireMock include un HEALTHCHECK per il proprio endpoint /__admin/health. La chiamata mappings elenca tutti gli stub letti dal server. Se uno stub creato da te non compare nell'elenco, non è stato caricato: verifica che il file si trovi in mappings/ e non nella directory root montata, quindi controlla che il JSON sia valido.

Quando arriva una richiesta e nessuno stub corrisponde, WireMock risponde con 404 e un corpo che inizia con Request was not matched, seguito da un confronto con lo stub più simile disponibile. Leggi il confronto prima di modificare qualsiasi cosa, perché indica il campo esatto che differisce. Di solito si tratta di un percorso contenente /v1/charge, mentre nello stub è indicato /v1/charges.

Scrivere il test come una sequenza con stato mantenuto tra le chiamate

I file Hurl sono semplici file di testo. Installate il pacchetto deb dalle release del progetto.

VERSION=8.0.1
curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VERSION/hurl_${VERSION}_amd64.deb
sudo apt update && sudo apt install ./hurl_${VERSION}_amd64.deb

Una suite che verifica la propria API rispetto al mock si trova in tests/checkout.hurl.

POST {{base_url}}/orders
Content-Type: application/json
{
  "sku": "ssd-1tb",
  "amount": 4200
}
HTTP 201
[Captures]
order_id: jsonpath "$['id']"

GET {{base_url}}/orders/{{order_id}}
HTTP 200
[Asserts]
jsonpath "$.status" == "paid"
jsonpath "$.charge_id" == "ch_test_001"

Il blocco [Captures] trasforma il test in un test API, anziché in due richieste non correlate. order_id viene letto dalla prima risposta e inserito nell'URL della seconda. L'asserzione su charge_id è il punto centrale dell'esercizio: dimostra che il servizio ha chiamato il provider di pagamenti e ha memorizzato la risposta, mentre il valore usato per il confronto è quello scritto nello stub WireMock. Un unico file copre ora entrambe le parti del flusso.

hurl --test --variable base_url=http://127.0.0.1:3000 \
  --report-junit reports/junit.xml \
  --report-json reports/json \
  tests/

Un'esecuzione completata correttamente stampa una riga per file e un riepilogo.

tests/checkout.hurl: Success (2 request(s) in 61 ms)
Executed files:    1
Executed requests: 2 (30.1/s)
Succeeded files:   1 (100.0%)
Failed files:      0 (0.0%)
Duration:          64 ms

In caso di errore, stampa error: Assert failure con il file e il numero di riga, quindi il valore ottenuto e quello atteso; hurl termina con un codice diverso da zero, così la CI si interrompe. Se status legge pending dove era atteso paid, il servizio non ha elaborato la risposta del mock. Il controllo successivo va eseguito nel journal delle richieste di WireMock, disponibile in /__admin/requests, che mostra se la chiamata ha raggiunto il mock.

Esegui la suite dal tuo runner CI

Con un runner registrato sullo stesso server, il workflow è breve. Il runner è un semplice processo sull'host, quindi docker e hurl devono essere installati su quell'host. Dal runner ospitato non viene ereditato nulla.

name: api-tests
on: [push]
jobs:
  hurl:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - name: Start the mock
        run: docker compose up -d --wait mock-payments
      - name: Run the suite
        run: hurl --test --variable base_url=http://127.0.0.1:3000 --report-junit reports/junit.xml tests/
      - name: Archive the reports
        if: always()
        run: install -d /srv/api-tests/reports/$GITHUB_SHA && cp -r reports/. /srv/api-tests/reports/$GITHUB_SHA/
      - name: Stop the mock
        if: always()
        run: docker compose down

if: always() nel passaggio di archiviazione è importante. Senza questa opzione, un'esecuzione dei test non riuscita salta la copia e perdi esattamente il report che volevi leggere. La copia deve inoltre essere eseguita fuori dalla workspace, perché il runner pulisce la workspace prima del job successivo e con essa elimina anche i report.

Conservare i risultati, non solo dell'ultima esecuzione

Un file XML JUnit per ogni commit risponde a una sola domanda: l'esecuzione è riuscita? Non indica quando un endpoint ha iniziato a rallentare, perché nessuno legge quei file dopo che si è smesso di aprirli. Per ottenere uno storico, aggiungere una riga per ogni esecuzione a un piccolo database sullo stesso server. È sufficiente una singola tabella con SHA del commit, nome del file, numero di test riusciti, numero di test falliti e durata. SQLite in produzione su un VPS è una scelta ragionevole: un solo file, nessun processo server e tutto lo storico viene incluso nel backup già eseguito. Analizzare l'output --report-json di Hurl invece dell'XML JUnit, perché tra i due è il formato leggibile dalle macchine.

Cosa deve sopravvivere alla ricostruzione di un container

Le definizioni dei mock e le suite di test sono codice sorgente. Devono trovarsi in un repository insieme al servizio che descrivono e devono essere modificate nella stessa pull request che modifica un endpoint. Uno stub modificato in un'interfaccia web, oppure un'aspettativa inviata a MockServer tramite la relativa API REST durante l'esecuzione, esiste soltanto nella memoria di quel container o nel database di quello strumento. Esegui docker compose down e il contenuto scompare. Nessuno se ne accorge finché un test non inizia a passare per il motivo sbagliato. Se i repository vengono eseguiti anche sul tuo hardware, un server git self-hosted mantiene fixture e servizio all'interno dello stesso perimetro di attendibilità.

Passiamo alle regole pratiche. Fissa i tag delle immagini, perché latest può cambiare il modo in cui il mock corrisponde alle richieste senza modifiche nel repository. Collegare questo problema alla causa è molto difficile. Monta le directory degli stub in sola lettura quando lo strumento non deve scrivervi. Non inserire mai gli stub di un mock in un volume Docker con nome. In caso contrario, il volume diventa la fonte autorevole e la copia in git diventa silenziosamente obsoleta.

Ce n'è un'altra, che spesso causa problemi. Se crei gli stub registrando traffico reale tramite un proxy, leggi ogni file generato prima di eseguire il commit. Una registrazione contiene esattamente ciò che l'upstream ha restituito, inclusi bearer token e indirizzi email dei clienti. Eseguire il commit di questi dati li inserisce permanentemente nel repository, perché git conserva nella cronologia anche il contenuto eliminato.

FAQ

Qual è la differenza tra un mock server per API e un test runner per API?

Un mock server risponde alle richieste. Sostituisce una dipendenza che non puoi chiamare da CI e non segnala mai se un test è superato o fallito. Un test runner per API invia richieste al tuo servizio, verifica le risposte, trasferisce i valori da una chiamata alla successiva e termina con un codice diverso da zero quando una verifica fallisce. Risolvono problemi diversi. In una configurazione tipica vengono eseguiti entrambi: il runner chiama il tuo servizio, mentre il tuo servizio chiama il mock.

Posso testare un'API interna da un runner CI ospitato?

Non senza esporla. Un runner ospitato si trova al di fuori della tua rete e non può raggiungere un servizio associato a un indirizzo privato. Le opzioni sono pubblicare l'API, eseguire un tunnel oppure mantenere una copia pubblica di staging. Ognuna aggiunge un componente che può guastarsi o causare una perdita di dati. Un runner nella stessa rete privata chiama direttamente il servizio. Questa è la principale ragione pratica per cui i team gestiscono internamente questo tipo di attività.

Dove devono risiedere gli stub dei mock e le suite di test delle API?

In git, accanto al servizio che descrivono. Gli strumenti che memorizzano le definizioni come file, ad esempio la directory mappings/ di WireMock, il file di dati di Mockoon, i file Hurl e la directory .bru di Bruno, permettono di eseguire la code review e di ricreare il container senza costi aggiuntivi. Gli strumenti che memorizzano le definizioni in un database o in una web UI richiedono un piano di backup e un passaggio di esportazione. L'esportazione è proprio la parte che spesso viene dimenticata, finché il container non è già stato eliminato.

Perché il mio mock restituisce 404 quando lo stub sembra corretto?

WireMock serve uno stub solo in caso di corrispondenza esatta. Una richiesta senza corrispondenza riceve 404 con un corpo che inizia con Request was not matched, seguito da un confronto con lo stub più vicino. Il confronto indica il campo che differisce. Le cause comuni sono una barra finale nel percorso, un header Content-Type richiesto dallo stub ma non inviato dal client, urlPath usato quando lo stub richiede urlPathPattern per un segmento variabile e un body matcher non compatibile con il payload. Controlla prima /__admin/requests per verificare che la richiesta abbia raggiunto effettivamente il mock.

Ho ancora bisogno dei mock se ho un ambiente di staging?

Sì, per due motivi. Anche una copia di staging di un upstream che non controlli può diventare non disponibile o applicare il rate limiting. Di conseguenza, la suite può fallire per motivi che non hanno alcuna relazione con il tuo codice. Inoltre, l'ambiente non può produrre le risposte che devi testare più spesso, ad esempio una carta rifiutata o un timeout del gateway. Un mock restituisce queste risposte su richiesta alla velocità della rete locale. Una suite che richiede minuti contro un sandbox può così completarsi in pochi secondi. Mantieni staging per il controllo finale prima del rilascio e usa i mock in CI.