Progettare la strategia di test in una pipeline significa decidere quale segnale di qualità viene raccolto, in quale punto del flusso, e chi può bloccare il rilascio quando quel segnale è negativo. Su AZ-400 la domanda non è “come aggiungo un task di test”, ma “dato questo requisito di qualità o governance, dove piazzo il controllo e chi lo amministra”. Diamo per acquisiti i meccanismi base e concentriamoci sulle decisioni di design e sui trade-off.
Piramide dei test: cosa gira dove
La prima decisione è collocare ciascuna categoria di test lungo il flusso, guidati dal rapporto fra tempo di feedback e fedeltà.
Gli unit test sono l’unica categoria che può stare nella build validation della pull request: deterministici, senza infrastruttura, questione di secondi. Se la suite supera i pochi minuti la risposta è parallelizzazione tramite job matrix o slicing, non lo spostamento a valle: rimandarli a dopo il merge sposta il difetto dove costa di più.
Gli integration test richiedono dipendenze reali - database, code, servizi esterni - e generano il trade-off più significativo. Le opzioni sono, tra le altre, contenitori effimeri creati nella pipeline stessa, ambienti di test condivisi, o un ambiente dedicato deployato dalla pipeline. L’ambiente condiviso è economico ma introduce contesa: due run concorrenti si corrompono i dati a vicenda. La risposta in Azure Pipelines è il check Exclusive lock sulla risorsa; il comportamento si governa con lockBehavior, dove sequential accoda tutti i run e runLatest - il default - lascia passare solo l’ultimo. sequential è corretto quando ogni run deve effettivamente essere eseguito, per esempio perché applica migrazioni di schema incrementali.
I load test hanno profilo opposto: lenti, costosi, con esiti statistici e non binari. Non appartengono alla PR validation; il pattern è eseguirli su un ambiente rappresentativo dopo il deploy in staging, come stage a sé. Con Azure Load Testing i criteri di superamento si dichiarano nel file YAML di configurazione del test tramite failureCriteria, con condizioni come avg(response_time_ms) > 300 o percentage(error) > 50, eventualmente riferite a una singola request. Vincolo di design chiave: dai workflow di Azure Pipelines e GitHub Actions si impostano criteri sulle metriche client, mentre quelli su metriche server-side non sono supportati da CI/CD e vanno configurati sulla risorsa dal portale. Esiste inoltre l’auto stop, che interrompe il test quando la percentuale di errore supera una soglia entro una finestra temporale.
Pubblicare i risultati: il segnale deve arrivare alla piattaforma
Eseguire i test non basta: il valore architetturale nasce quando i risultati diventano dati di piattaforma, interrogabili per trend, tracciabilità e ownership dei fallimenti.
Il task Publish Test Results accetta i formati JUnit, NUnit, VSTest, XUnit e CTest, dove VSTest indica il formato TRX. Copre di fatto qualunque ecosistema, perché la scelta si fa sul formato di output e non sul linguaggio: un runner Python o Node che emette JUnit XML è pubblicabile senza task custom. Attenzione però: alcuni task, tra cui Visual Studio Test e .NET Core CLI, pubblicano già i risultati, e un secondo task duplica i test run.
Due input hanno peso decisionale sproporzionato. failTaskOnFailedTests ha default false: il task pubblica i risultati e non fallisce il job anche con test rossi. È la trappola classica della pipeline “verde” con test falliti, e va impostato a true quando il fallimento deve bloccare. mergeTestResults decide se i file confluiscono in un unico test run o ne generano di separati: unificare è preferibile quando provengono dallo stesso framework, perché mappatura e durata risultano calcolate correttamente.
Il tema correlato è la flakiness: un test intermittente erode la fiducia nella pipeline più di un test mancante, perché insegna al team a ignorare il rosso. Azure DevOps offre il flaky test management con rilevamento di sistema - un test che fallisce e passa alla riesecuzione viene marcato flaky - oppure custom via API. La decisione di design sta nel reporting: usare il tag flaky solo per troubleshooting, oppure escludere i flaky dal test summary sopprimendone il fallimento. La seconda scelta sblocca il flusso ma nasconde debito.
Code coverage: segnale di design, non trofeo
La coverage totale è quasi inutile come gate: alta su codice legacy ben testato, bassa dopo ogni refactor legittimo. La distinzione che conta è fra full coverage, sull’intera codebase, e diff coverage, calcolata solo sulle righe modificate dalla pull request. Solo la seconda è un criterio equo per bloccare un merge, perché misura la disciplina del cambiamento in esame e non il passato del repository.
Su Azure Pipelines la coverage si pubblica con Publish Code Coverage Results v2, che per Cobertura, JaCoCo e altri formati XML genera un report HTML navigabile, mentre per i formati .coverage produce una vista tabellare più povera. La configurazione della diff coverage però non vive nella pipeline: si dichiara in un file azurepipelines-coverage.yml alla radice del repository. La soglia appartiene al repository, quindi resta valida qualunque pipeline compili quel codice.
Il punto che decide le domande d’esame è che lo status check di coverage è advisory per default: segnala, non blocca. Per trasformarlo in vincolo serve una branch policy che lo richieda, secondo la convenzione nome-pipeline/codecoverage. In una pipeline YAML multistage i risultati sono inoltre disponibili solo a pipeline conclusa: per valutarli prima del deploy in produzione, la build va isolata in una pipeline propria.
Quality gate e release gate: chi controlla il controllo
Qui sta la differenza concettuale più importante dell’argomento. In Azure Pipelines i controlli sulle risorse - approvals and checks - non sono definiti nel file YAML: li amministra il proprietario della risorsa dall’interfaccia web, e chi modifica il YAML non può alterarli. È questa separazione a renderli strumento di governance e non di build, soddisfacendo requisiti di separazione dei compiti che nessuna configurazione nel repository potrebbe garantire.
I check si configurano su risorse tra cui environment, service connection, repository, variable group, secure file e agent pool, e sono valutati in ordine definito: prima i check statici - Branch control, Required template, Evaluate artifact - poi le approvazioni pre-check, quindi i check dinamici tra cui Approval, Invoke Azure Function, Invoke REST API, Business hours e Query Azure Monitor alerts, infine le approvazioni post-check e l’Exclusive lock. La mappatura requisito-controllo è diretta:
- “Solo artefatti costruiti da branch protetti raggiungono la produzione” conduce a Branch control, che verifica il branch di origine di tutte le risorse collegate e può pretendere protezione attiva.
- “Ogni pipeline che usa questa service connection deve eseguire i nostri step di sicurezza” conduce a Required template: la pipeline fallisce se non estende il template indicato. È il modo per imporre scansioni senza fidarsi del YAML applicativo.
- “L’immagine container va valutata contro una policy prima del deploy” conduce a Evaluate artifact, che al momento opera su artefatti di tipo container image.
- “Nessun alert attivo dopo un rollout canary” conduce a Query Azure Monitor alerts, il gate osservativo tipico dei rollout progressivi.
- “Integrazione con un sistema esterno di change management o scansione” conduce a Invoke REST API o Invoke Azure Function, con rivalutazione periodica.
Nelle classic release pipeline l’equivalente sono i gate pre e post-deployment, con ritardo iniziale, intervallo di campionamento e timeout: il ritardo esiste perché una metrica raccolta subito dopo un deploy non è ancora stabile. Per l’intervento umano, il task Manual Intervention funziona solo in un agentless job delle classic release, mentre in YAML il pattern equivalente è Manual Validation su un job con pool: server.
Su GitHub Actions lo stesso obiettivo si raggiunge con gli environment e le loro deployment protection rule: required reviewers - fino a sei persone o team, con una sola approvazione necessaria per procedere - wait timer, policy sui branch e sui tag di deployment, e regole custom fornite da GitHub App. Anche qui il controllo vive nelle impostazioni del repository, non nel workflow.
Come si presenta all’esame
Le domande su questo dominio raramente chiedono la sintassi di un task: chiedono di collocare un controllo, e il discriminante ricorrente è la proprietà. “Il team di sviluppo non deve poter aggirare il controllo” porta a un check sulla risorsa o a una branch policy, mai a uno step nel YAML. “Deve valere per tutte le pipeline che usano quella risorsa” porta a Required template o a un check sulla service connection. “Solo per le righe modificate” porta alla diff coverage configurata nel repository, ricordando che lo status da solo è advisory. E la performance sotto carico valutata in automatico porta ai criteri di fallimento su metriche client, non server-side.