Vai al contenuto
DedicatedPHP Contatto

Test di contratto per API PHP senza rompere le integrazioni

Scopri come definire e automatizzare contratti verificabili per evolvere le API PHP senza scoprire incompatibilità quando la modifica è già distribuita.

Diagramma editoriale di un'API PHP che convalida contratti di richieste e risposte prima della distribuzione

Un'API non è definita soltanto dal suo controller PHP né da una specifica pubblicata. È definita anche dalle aspettative che altri sistemi hanno già incorporato: una route, il nome di un campo, un codice HTTP, il formato di un errore o l'ordine necessario per scorrere le pagine. I test di contratto per API PHP trasformano tali aspettative in verifiche automatizzate prima di effettuare il merge del codice o distribuirlo.

L'obiettivo non è impedire ogni evoluzione. È rilevare se una modifica altera un accordo osservabile da un consumer e deciderlo consapevolmente: mantenere la compatibilità, introdurre una transizione o versionare l'interfaccia. Ciò è particolarmente importante nelle API interne con più team, nelle integrazioni B2B e nei flussi asincroni in cui il malfunzionamento può comparire ore dopo la pubblicazione.

Cosa risolvono i test di contratto e cosa non sostituiscono

Cosa risolvono i test di contratto e cosa non sostituiscono — guía visual de DedicatedPHP

Un test di contratto verifica che provider e consumer siano allineati su un'interazione: data una richiesta valida, il provider produce una risposta con una struttura, tipi e regole concordati. In senso inverso, un consumer può dichiarare quali richieste gli occorrono e il provider verifica di poterle gestire.

Questo approccio rileva incompatibilità che i test unitari tendono a trascurare. Un test unitario può confermare che un serializzatore restituisce customer_id; non dimostra che il consumer continui a comprendere quel campo se prima si aspettava customerId. Un test di integrazione locale può coprire l'endpoint, ma non raccoglie necessariamente le assunzioni reali di ogni integrazione.

Non sostituiscono altri controlli:

  • Test unitari, per regole di dominio, validazione e trasformazioni.
  • Test di integrazione, per database, code, cache, autenticazione o servizi connessi.
  • Test end-to-end, per percorsi critici completi in ambienti controllati.
  • Test di sicurezza e prestazioni, per autorizzazione, abusi, esposizione dei dati, latenza e capacità.
  • Osservabilità in produzione, per rilevare consumer che usano ancora comportamenti in dismissione.

Un contratto non certifica neppure che la risposta sia corretta per il business; certifica che conserva la forma e la semantica dichiarate. Per questo, deve essere accompagnato da esempi che esprimano regole rilevanti, non soltanto schemi vuoti.

Cosa fa parte del contratto di un'API

Il contratto è qualsiasi comportamento che un consumer possa osservare e da cui dipenda. Limitarlo al JSON di una risposta riuscita lascia fuori le rotture più frequenti. Per ogni operazione, è opportuno concordare almeno i seguenti elementi.

  • Richiesta: metodo, route, parametri di query, header, body, campi obbligatori, formati e limiti.
  • Risposta: codice HTTP, header rilevanti, struttura, tipi, campi opzionali, campi che ammettono valori null e formati di data, valuta o identificatori.
  • Errori: codici di stato, body dell'errore, codice funzionale stabile e condizioni che lo generano. Un testo per le persone può cambiare; un codice come validation_failed è più adatto all'automazione.
  • Paginazione e filtraggio: significato di limit, cursore o pagina, stabilità dell'ordine, rappresentazione del cursore successivo e gestione degli insiemi vuoti.
  • Autenticazione e autorizzazione: meccanismo ammesso, header richiesti, ambiti o permessi e differenza tra credenziali non valide, assenza di credenziali e accesso negato.
  • Eventi e webhook: nome dell'evento, versione o schema del payload, firma, tentativi ripetuti, identificatore dell'evento, ordine non garantito e aspettative di idempotenza.

L'obbligatorietà di una proprietà e l'ammissione di null sono regole indipendenti. Un campo può essere obbligatorio e ammettere null, opzionale e non ammetterlo quando compare, oppure opzionale e ammetterlo se presente. Allo stesso modo, una proprietà omessa, una proprietà presente con null e una proprietà con stringa vuota sono stati distinti. Se il consumer li interpreta in modo diverso, il contratto deve esprimerlo e verificarlo.

Piccole modifiche che possono rompere i consumer

Una modifica può sembrare innocua dal punto di vista del provider ed essere incompatibile per un client generato, un validatore rigoroso o una logica di business. Modificare un intero in una stringa, ad esempio da 42 a "42", rompe confronti e schemi. Rendere un campo opzionale non determina di per sé se accetta null: la prima regola definisce se la proprietà deve essere presente, mentre la seconda definisce i valori validi quando è inclusa.

Altre modifiche rischiose sono restituire 200 dove prima veniva restituito 201, sostituire una lista vuota con null, modificare la precisione di un decimale, rinominare un codice di errore o cambiare l'ordine di paginazione senza avvisare. Aggiungere un campo è generalmente compatibile per lettori tolleranti, ma non lo è se un consumer valida uno schema chiuso o calcola firme sull'intero body.

La compatibilità dipende dall'accordo reale, non da una regola isolata. È opportuno classificare ogni modifica in base ai consumer conosciuti, alla tolleranza dichiarata e alla criticità del flusso. Se tali informazioni non sono note, devono essere trattate come un rischio e non come un'assunzione favorevole.

Scegliere tra specifica, contratti del consumer o entrambi

Una specifica di interfaccia, ad esempio una descrizione OpenAPI, funziona bene come fonte comune per route, operazioni, parametri, schemi e risposte. Può essere validata nella pipeline per rilevare modifiche incompatibili rispetto a una versione di riferimento. È utile quando ci sono molti consumer o quando client e documentazione vengono generati dalla stessa definizione.

Tuttavia, uno schema non sempre cattura ciò che conta per ciascun consumer: combinazioni di filtri, un errore specifico in presenza di una condizione di business o una dipendenza da un valore di esempio. I contratti guidati dai consumer dichiarano interazioni concrete di cui ciascun consumer ha bisogno. Il provider li verifica rispetto alla propria implementazione.

Usare entrambi i livelli è spesso ragionevole: la specifica governa la superficie generale e i contratti del consumer coprono flussi di alto valore o una semantica difficile da ridurre a uno schema. Deve esistere una responsabilità chiara per ogni artefatto. Se una specifica non viene aggiornata quando cambia il codice, smette di essere una fonte di verità e diventa documentazione fittizia.

Esempi rappresentativi e casi limite

Un esempio di contratto deve contenere dati realistici nella struttura, non dati di produzione. Per una risorsa ordine, è opportuno includere un caso con elementi, uno vuoto se valido, identificatori nel formato concordato e date complete con fuso orario quando questa sia la convenzione. Vanno aggiunti casi di autorizzazione negata, validazione fallita, risorsa inesistente e paginazione finale.

È opportuno evitare di fissare dettagli irrilevanti che cambiano legittimamente, come un identificatore casuale, l'ora corrente o l'ordine delle proprietà JSON. Vanno usate asserzioni precise su ciò che è stabile e tolleranza esplicita su ciò che è variabile. Ogni esempio deve rispondere a un'esigenza nota; una raccolta enorme di risposte inventate aumenta la manutenzione senza accrescere la fiducia.

Implementazione progressiva in un'API PHP esistente

Non è necessario modellare l'intera API prima di ottenere valore. Si inizi con un inventario dei consumer: applicazioni interne, client B2B, processi batch, applicazioni mobili, automazioni e ricevitori di webhook. Vanno registrati responsabile, canale di contatto, operazione utilizzata, criticità e capacità di aggiornamento.

Successivamente, occorre dare priorità agli endpoint che creano o modificano risorse, autenticano utenti, alimentano processi finanziari o attivano automazioni. Va stabilita una baseline del comportamento attuale tramite una specifica revisionata e test contro un'istanza riproducibile dell'API. In PHP, il test deve esercitare il livello HTTP reale dell'applicazione, non invocare direttamente una classe di servizio: il contratto include routing, middleware, serializzazione e gestione delle eccezioni.

POST /api/orders
Authorization: Bearer token
Content-Type: application/json

{"items":[{"sku":"ABC-1","quantity":2}]}

201 Created
{"id":"ord_123","status":"pending","items":[...]}

L'esempio precedente è utile soltanto se è accompagnato da regole: quali campi sono obbligatori, se id è sempre una stringa, quali errori restituisce uno SKU non valido e se lo stato iniziale è garantito. Sono tali regole che devono trasformarsi in asserzioni.

Validazione nell'integrazione continua e prima della distribuzione

La pipeline deve fallire prima del merge se l'implementazione non rispetta contratti approvati. Un flusso pratico include l'esecuzione di test unitari, l'avvio di dipendenze controllate, l'avvio dell'API PHP con configurazione di test e la validazione della specifica, dei contratti del provider e dei contratti di consumer rappresentativi. I test devono usare dati isolati e deterministici affinché un fallimento sia riproducibile.

In una richiesta di modifica, occorre inoltre confrontare la specifica proposta con la versione pubblicata per segnalare rimozioni di route, irrigidimento dei requisiti di presenza, modifiche nell'ammissione di null, modifiche di tipo e risposte rimosse. La diagnosi deve indicare operazione, interazione e regola non rispettata; un semplice errore di schema obbliga a un'indagine eccessiva.

Prima della distribuzione, va eseguita la stessa suite sull'artefatto che verrà pubblicato, non su una build diversa. Dopo la distribuzione, vanno monitorati codici di errore, fallimenti di deserializzazione segnalati dai client, uso delle versioni e traffico verso route deprecate. La validazione preventiva riduce il rischio; non sostituisce la conferma del comportamento sotto traffico reale.

Compatibilità, deprecazione e ritiro sicuro

Compatibilità, deprecazione e ritiro sicuro — guía visual de DedicatedPHP

Quando una modifica non è compatibile, è preferibile una transizione esplicita. È possibile aggiungere un nuovo campo mantenendo il precedente, introdurre una nuova operazione o versione e comunicare una data di ritiro supportata da segnali di utilizzo. La deprecazione è un periodo operativo con responsabili, comunicazione e misurazione; non soltanto una nota nella documentazione.

Non va ritirato un comportamento perché è trascorso un termine se non è possibile identificare consumer che dipendono ancora da tale comportamento o se il flusso è critico. Quando possibile, vanno esposti avvisi controllati e metriche per individuare l'uso precedente senza alterare la risposta. L'esposizione graduale di una nuova versione consente di osservare errori e correggere i contratti prima di estendere la modifica.

Gli errori più comuni sono testare soltanto risposte riuscite, modellare messaggi umani anziché codici di errore, assumere che tutti i client ignorino i campi nuovi e non coinvolgere consumer reali o rappresentativi. I test di contratto apportano valore quando riflettono accordi mantenuti da entrambe le parti e vengono eseguiti come una normale condizione di consegna.

Vuoi applicare queste idee al tuo progetto?Parliamo della tua piattaforma PHP.
Visualizza il servizio correlato