Atto V: L'apparenza del completamento
Testare l'intestabile
Un build Plus moderno è quattro problemi di testing impilati uno accanto all’altro, e la cosa che inquieta è che nessuno di loro è di prima classe. Shopify ha passato anni a costruire una delle piattaforme di commerce più estensibili in circolazione. Puoi riscrivere il checkout. Puoi iniettare la tua logica nel motore dei prezzi e degli sconti. Puoi renderizzare componenti nativi dentro l’admin. Puoi far girare un’app custom completa che dialoga con ogni angolo della piattaforma. È una quantità di superficie davvero notevole da ricevere in mano.
Quello che Shopify non ha costruito, nemmeno lontanamente allo stesso livello, è una storia di testing.
Quel divario una volta era una nota a piè di pagina. Adesso non lo è più, perché il build che lo innesca è ormai il build normale. Uno store Plus serio, oggi, abbraccia di routine temi che portano vera logica di business, una manciata di Functions, una o due extension di checkout o admin, e un’app custom che riceve webhook. In un normale progetto Rails o Next.js hai un solo stack di testing per tutto—un runner, un sistema di fixture, un insieme di convenzioni che tutti conoscono già. Qui hai quattro risposte diverse, e nessuna è di prima classe. In nessuna di esse controlli i tuoi dati. In nessuna di esse giri nel tuo ambiente. Il tooling progettato esattamente per la tua situazione esiste, a essere generosi, per uno dei quattro.
All’engineering manager che ha messo a budget questa migrazione partendo da una proposta con un’unica voce chiamata testing: quella voce è quattro voci. Due di esse sono improvvisate. Una non esiste affatto. Niente di tutto questo è un motivo per farsi prendere dal panico—è gestibile, tutto quanto, e ci sono team che ogni settimana mandano in produzione store Plus testati magnificamente. Ma il budget che la proposta ha quotato era il budget per testare un’applicazione normale, e questa non è un’applicazione normale. Meglio scoprirlo qui che nella retro.
Comincia dal layer dove le notizie sono peggiori, perché tutto il resto, al confronto, sembra generoso. Shopify non rilascia alcun framework di test per i temi. Nessun runner, nessun helper, nessun sistema di fixture, niente. Ti viene messo in mano del Liquid che compila e renderizza da qualche parte dentro la CDN di Shopify, mai nelle tue mani a runtime, e ti viene detto, in sostanza, buona fortuna.
L’approccio che funziona—quello a cui ricorriamo per primo—è Playwright che gira contro un preview theme. Quando si apre una pull request, generi un preview theme a partire da quello live, ci punti Playwright contro, e fai girare le cose che contano davvero: controlli di accessibilità, flussi end-to-end nel browser, un checkout vero completato con i numeri delle carte di test di Shopify. I fallimenti bloccano il merge. È un setup solido, e per i flussi che muovono denaro è quello giusto.
Funziona entro un limite preciso, e quel limite è tutta la storia di questo layer. L’ambiente di test è di sola lettura. Puoi navigare lo store, riempire un carrello, completare un checkout—ma non puoi creare dati di test. Se il comportamento che devi verificare dipende da una specifica configurazione di prodotto, quel prodotto deve già esistere nello store nel momento in cui il test gira. Se non esiste, non hai un modo più lento di testare lo scenario. Non hai alcun modo di testare lo scenario.

La mitigazione parziale è uno staging store: uno store dedicato, riempito a mano con un set curato di prodotti che coprono le configurazioni che ti interessano. Questo aiuta davvero. Ma introduce anche un problema più silenzioso e più corrosivo di quello che risolve, perché gli staging store vanno alla deriva. Una configurazione di prodotto che esiste in staging ma non in produzione—o in produzione ma non in staging—non si annuncia. Genera un falso positivo, una spunta verde contro un mondo che non corrisponde più a quello reale, e lo fa silenziosamente, e lo fa tanto più spesso quanto più a lungo lo store vive. La suite continua a passare. Ciò contro cui passa lentamente smette di essere vero.
Per gli snippet in cui la logica si dirama abbastanza da farti voler esercitare venti combinazioni invece di due, Playwright è troppo grossolano e troppo lento. Così il nostro team ha costruito e rilasciato in open source minitest_shopify_themes, una piccola libreria che renderizza snippet e section dei temi in isolamento usando Minitest e Capybara, con fixture che inietti direttamente invece di dipendere dallo stato dello store. Passi i dati, fai le asserzioni sull’output renderizzato, e lo fai per quante combinazioni vuoi, in modo rapido e deterministico. La chiamiamo un esperimento perché onestamente è quello che è. Copre bene ciò che copre; i blocchi dinamici e il rendering tra snippet si comportano male nel suo ambiente di test; e siccome Shopify non benedice niente di tutto questo, non c’è alcuna promessa che sopravviva al prossimo cambiamento della piattaforma. La usiamo come useresti qualunque strumento affilato che ti sei fabbricato da solo—con intenzione, per i tagli in cui è davvero bravo, con Playwright che continua a fare il lavoro strutturale sotto.
Dopo il layer dei temi, le Functions sembrano una piattaforma diversa costruita da persone diverse che ti volevano più bene. Questo è il punto luminoso, ed è luminoso proprio per una decisione di design, non per una decisione di tooling.
Le Functions compilano in WebAssembly e girano come funzioni pure. Entra GraphQL, esce GraphQL, e in mezzo non c’è rete, non c’è stato esterno, niente a cui attingere e niente da mockare. L’intera funzione è la sua stessa fixture di test. Non c’è un mondo da costruirle attorno perché non dipende da un mondo—dipende solo dal suo input, e il suo input è un valore che puoi scrivere a mano. Importi la Function direttamente in un file di test, la chiami come qualunque altro pezzo di codice, le passi un input, e fai l’asserzione sull’output. È tutta qui la cerimonia.
Shopify ci si appoggia. Uno strumento di type-generation, typegen, legge la tua query GraphQL ed emette tipi TypeScript a partire dallo schema reale di Shopify, così il tuo editor fa l’autocomplete contro la realtà e il compilatore intercetta un mismatch strutturale prima che possa arrivare in produzione. Colleghi i test alla CI, blocchi il deploy a seconda del loro esito, e una Function i cui test falliscono semplicemente non va in produzione. Per una volta la piattaforma fa la sua parte.
Vale la pena nominare il limite con franchezza, perché è facile riporre troppa fiducia in un layer che finalmente si comporta bene. Questo è unit testing, e solo unit testing. Ti dice che la Function è corretta in astratto. Non ti dice che la Function è corretta contro dati Shopify reali in uno store reale, perché il vincolo del seeding del layer dei temi non se n’è mai andato—se per innescare l’edge case serve che esista davvero una particolare configurazione di carrello o di prodotto, nemmeno qui puoi farla apparire dal nulla. Per quegli scenari l’unit test è il soffitto, e dovresti sapere che ci stai poggiando i piedi sopra.
Le UI Extension sono il punto in cui la documentazione finisce del tutto e ti ritrovi a costruire al buio, con le mani. Extension di checkout, extension di order-status, extension di admin—sono componenti React che renderizzano dentro iframe che Shopify serve e che Shopify possiede. Puoi raggiungerne uno da un test Playwright. Non puoi controllare il contesto in cui gira. Non c’è un percorso di testing ufficiale, nessuno, nemmeno uno sbagliato con cui prendersela.
Così ce ne siamo inventati uno, e vogliamo essere precisi sulla parola inventati. Mockiamo le API della libreria delle extension di Shopify a livello di modulo—mock globali che permettono al codice dell’extension di girare in isolamento, lontano dall’host di Shopify, così la logica di rendering e il comportamento condizionale e le letture dei metafield possono essere esercitati senza un checkout live in attesa. Non siamo a conoscenza di alcuna documentazione che descriva tutto ciò. L’abbiamo ricavato dalla forma della libreria e da anni passati a mockare API host simili in altri contesti. Per un’extension che porta vera logica, la copertura è significativa, ed è parecchio meglio di niente.
Il rischio è deliberato, ed è del tipo che non bussa prima di entrare. Se Shopify cambia il comportamento della libreria delle extension, i nostri mock non cambiano con essa. La deriva è silenziosa: i test restano verdi mentre la produzione, in silenzio, diverge dalla cosa in cui i test credono. Abbiamo accettato quel compromesso a occhi aperti, perché l’alternativa non è migliore. I test end-to-end contro un checkout Shopify reale sono più lenti, più instabili, e restano comunque inchiodati sotto lo stesso vincolo del seeding—se il comportamento dell’extension dipende da uno specifico stato del carrello o dal valore di un metafield, quello stato non puoi comunque costruirlo in modo affidabile al momento del test. Qui non esiste una risposta pulita. Scegli il compromesso con cui preferisci convivere, e ti fai una ragione del fatto che stai facendo una scelta, non trovando una soluzione.
Il layer dell’app è il punto in cui finalmente torni a sentirti competente, più o meno. È la superficie più familiare di tutto lo stack: un’applicazione web standard, un framework maturo—Rails e RSpec, nel nostro caso—fixture dei payload dei webhook, mocking delle richieste HTTP, tutto l’apparato confortevole che già sai far funzionare. Buona parte è priva di sorprese, e l’assenza di sorprese è un lusso dopo le ultime tre sezioni.
Ci sono due trappole, ed entrambe sono del tipo silenzioso, che su questo layer è l’unico tipo da cui valga la pena metterti in guardia.
La prima è il versioning dei webhook. Un webhook di Shopify porta la struttura del payload della versione di API contro cui la tua app è configurata, e le tue fixture rispecchiano quella struttura fedelmente—nel momento in cui le scrivi. Poi Shopify deprecata una vecchia versione e tu fai l’upgrade, come prima o poi devi, e i tuoi payload mockati possono allontanarsi in silenzio da ciò che Shopify ora invia davvero. I test continuano a passare. Stanno passando contro payload che non riflettono più la realtà, ed è un verde più pericoloso di un rosso, perché un rosso almeno chiede la tua attenzione. Intercettare questa cosa non è automatico; è una disciplina. Ogni singola volta che alzi una versione di API, tiri su le fixture dei webhook, le confronti con lo schema della nuova versione, e aggiorni ciò che è cambiato. Mettilo nella checklist di upgrade, perché non ci si metterà da solo.
La seconda trappola ha un nome e una piccola confessione attaccata. AppBridge—il framework di Shopify per renderizzare UI nativa dell’admin dentro la tua app—si affida al contesto dell’iframe dell’admin per autenticarsi e renderizzare, e quel contesto non puoi replicarlo in un ambiente di test. Non c’è un modo onesto di testare un componente cablato ad AppBridge in quanto tale. L’unico percorso che abbiamo trovato che funziona davvero è rifiutarsi, in primo luogo, di mettere qualcosa che valga la pena testare dentro quei componenti: estrai la logica di business fuori, senza pietà, finché AppBridge non è un sottile strato di presentazione e tutto ciò che conta vive sotto, in codice testabile e semplice. (Chiedici come lo sappiamo. Lo sappiamo perché una volta abbiamo intrecciato la logica e la UI dentro i componenti AppBridge, l’abbiamo mandato in produzione, e poi abbiamo dovuto separarli più tardi sotto scadenza, che è il momento più costoso in assoluto per imparare quella lezione.) AppProxy, per quel che vale, è il cugino facile—quegli endpoint sono semplicemente API, quindi testali come qualunque altra API e tira dritto.
E ora il layer che non esiste, che è quello che ti farà davvero male, perché è invisibile fino all’istante esatto in cui non lo è più.
Ogni layer qui sopra può essere testato, in qualche misura, in qualche modo. Ciò che non può essere testato con alcuna affidabilità è come i layer si comportano insieme. Percorri il tragitto: una Function modifica le line item del carrello, una checkout extension legge quelle line item per decidere cosa mostrare, il tema renderizza un elemento che riflette l’output dell’extension, e l’app registra l’evento d’ordine che ne risulta. Quattro layer, un solo percorso utente. Testa ogni layer in isolamento e tutti e quattro diventano verdi. Se poi si compongano correttamente—in condizioni reali, con dati reali, in un checkout reale—è una domanda completamente diversa a cui nessuna di quelle quattro spunte verdi ha risposto.
È il problema più antico dei sistemi distribuiti vestito con gli abiti di Shopify: ogni componente può essere individualmente corretto mentre il sistema nel suo insieme è sbagliato. L’ecosistema di Shopify è vasto e composizionale per progettazione—quella componibilità è tutto il suo senso, il motivo per cui hai scelto la piattaforma. Ma componibilità e testabilità tirano in direzioni opposte. Più liberamente i pezzi si combinano, più sono le combinazioni che nessun test ha mai visto, e in uno stack distribuito così, il vero integration testing end-to-end è lento, fragile, e costoso da mantenere—spesso così costoso che mantenerlo onestamente non è una cosa che qualcuno finisca davvero per fare.
Ed ecco la frase che il capitolo precedente ti ha insegnato a tenere d’orecchio, ripresa in una nuova tonalità. Shopify gestisce parzialmente l’integrazione tra i layer—il che vuol dire che non la gestisce affatto, e ti consegna la cucitura da gestire da solo. Questo problema non lo risolvi. Lo gestisci. La distinzione non è un premio di consolazione; è l’istruzione vera e propria. Risolvere implica uno strumento che non hai ancora trovato, e non c’è uno strumento del genere da trovare. Gestire implica una pratica, e la pratica è concreta: scrivi contratti chiari tra i layer, così ciascuno dichiari cosa garantisce sulla forma di ciò che emette. Tieni un change management disciplinato ogni volta che un singolo layer si muove, perché un cambiamento localmente sicuro può essere globalmente distruttivo. E mantieni una piccola, mirata suite di smoke test end-to-end solo sui percorsi a maggior rischio—aggiungi al carrello, checkout, conferma d’ordine—la manciata di tragitti a cui non si può permettere di rompersi, accettando che questa suite ti dà un segnale anziché una certezza. Un segnale sui percorsi che contano vale più di una certezza che non avresti mai ottenuto.
La copertura completa su uno stack Plus non è in offerta. È importante dirlo senza esitare, perché metà dell’ansia in questo lavoro nasce dal rincorrere un numero che la piattaforma, strutturalmente, non ti lascerà raggiungere, e dal trattare quel mancato traguardo come un fallimento personale. Non lo è. È una proprietà del territorio.
Cosa significa fare le cose bene, qui, non è una percentuale di copertura. È sapere, layer per layer, esattamente che tipo di fiducia hai in mano: copertura reale sulle Functions, che sono genuinamente testabili e su cui vale la pena investire pesantemente; copertura consapevolmente fragile e improvvisata sulle UI Extension, che tieni e sorvegli e di cui non ti fidi più di quanto l’hai costruita; e segnale-non-certezza sull’end-to-end dei temi e sull’integrazione tra i layer, dove operi con la smoke suite, i contratti e la tua stessa paranoia. Fare le cose bene non è far finta che quelle tre cose siano la stessa cosa. Fare le cose bene è sapere su quale delle tre stai poggiando i piedi in ogni dato momento.
E l’unica decisione che si accumula su tutti e quattro i layer, la cosa più vicina a una legge universale che questo capitolo abbia: scrivi il codice in modo che possa essere esercitato in isolamento. Tieni la logica di business lontana dal rendering. Tieni la logica di rendering lontana dal contesto host di Shopify. Tieni la parte che conta estraibile dalla parte che non controlli. Il codice testabile in isolamento è l’unico codice che puoi testare in modo affidabile, perché su gran parte di questo stack l’isolamento è il miglior ambiente che otterrai mai—e il codice che hai scritto per essere isolabile è codice che puoi ancora coprire quando ogni altra strategia sbatte contro il muro della sola lettura.
I team che testano bene Shopify non sono quelli che hanno trovato strumenti migliori. Non ci sono strumenti migliori in attesa di essere trovati; abbiamo guardato. Sono quelli che hanno smesso di aspettare che Shopify consegnasse loro un framework e hanno iniziato a costruirsene uno proprio—un gate Playwright su preview theme, un renderer di snippet fatto in casa che non promettono sopravviva alla prossima release, mock a livello di modulo ricavati dalla forma di una libreria, una smoke suite a guardia dei tre percorsi che muovono il denaro. Niente di tutto questo è di prima classe. È stato tutto costruito da persone che hanno deciso che l’assenza di una risposta ufficiale non era la stessa cosa del permesso di andare in produzione alla cieca. Sii uno di quei team. Il budget è più grande di quanto diceva la proposta, le risposte sono in parte inventate, e il lavoro è del tutto fattibile—in quest’ordine, e a occhi aperti.