Test del codice in una pipeline GitLab prima del deploy in produzione

In un processo di sviluppo moderno, nessuna modifica dovrebbe raggiungere l'ambiente di produzione senza essere passata attraverso una pipeline di test automatizzati. Con GitLab CI/CD possiamo definire queste verifiche in modo dichiarativo nel file .gitlab-ci.yml, garantendo che ogni commit e ogni merge request rispettino standard di qualità e stabilità.

Obiettivi dei test nella pipeline

  • Individuare i bug il prima possibile: fallire velocemente su commit difettosi.
  • Proteggere l'ambiente di produzione: evitare regressioni e downtime.
  • Standardizzare il processo di verifica: tutti gli sviluppatori passano dagli stessi controlli.
  • Tracciare la qualità nel tempo: metriche ripetibili e storicizzate.

Struttura tipica di una pipeline GitLab

Una pipeline GitLab è composta da stages e jobs. Gli stages rappresentano le fasi logiche (ad esempio: lint, test, build, deploy), mentre i jobs sono i singoli passi eseguiti in ciascuna fase.

stages:
  - lint
  - test
  - build
  - deploy

Per testare il codice prima del deploy in produzione, è fondamentale che gli stages di test siano obbligatori e che lo stage di deploy venga eseguito solo se i precedenti hanno avuto successo.

Tipologie di test nella pipeline

1. Linting e formattazione

Questa è la prima linea di difesa: controlla stile, sintassi e pattern sospetti.

  • Linting: ESLint per JavaScript/TypeScript, Pylint o Flake8 per Python, RuboCop per Ruby, ecc.
  • Formattazione automatica: Prettier, Black, gofmt e altri.
lint:
  stage: lint
  image: node:22
  script:
    - npm ci
    - npm run lint
  only:
    - merge_requests
    - main

2. Test unitari

I test unitari verificano il comportamento delle singole unità di codice (funzioni, classi, componenti). Sono veloci da eseguire e dovrebbero coprire la maggior parte dei casi.

unit_tests:
  stage: test
  image: node:22
  script:
    - npm ci
    - npm test -- --ci --reporters=junit
  artifacts:
    when: always
    paths:
      - junit.xml
    reports:
      junit: junit.xml
  only:
    - merge_requests
    - main

3. Test di integrazione

Verificano l'interazione tra componenti diversi: ad esempio backend e database, oppure servizi esterni mockati. In genere sono più lenti dei test unitari e potrebbero richiedere servizi ausiliari (database, code, ecc.).

integration_tests:
  stage: test
  image: python:3.12
  services:
    - name: postgres:16
      alias: db
  variables:
    POSTGRES_DB: app
    POSTGRES_USER: app
    POSTGRES_PASSWORD: secret
  script:
    - pip install -r requirements.txt
    - pytest tests/integration --junitxml=integration.xml
  artifacts:
    when: always
    reports:
      junit: integration.xml

4. Test end-to-end (E2E)

I test E2E simulano i flussi reali dell'utente. Spesso vengono eseguiti contro un ambiente di staging o un'istanza effimera della nostra applicazione.

e2e_tests:
  stage: test
  image: cypress/included:13.8.1
  script:
    - npm ci
    - npm run start:test &
    - npx cypress run
  artifacts:
    when: always
    paths:
      - cypress/videos
      - cypress/screenshots

5. Test di sicurezza e qualità del codice

È una buona pratica integrare scansioni di sicurezza e analisi statica del codice nella pipeline:

  • Dependency scanning (vulnerabilità nelle librerie).
  • SAST (Static Application Security Testing).
  • Controllo licenze.
sast_scan:
  stage: test
  image: registry.gitlab.com/gitlab-org/security-products/sast:latest
  script:
    - /analyzer run
  artifacts:
    reports:
      sast: gl-sast-report.json

Controllare il flusso fino al deploy

Per garantire che il deploy in produzione avvenga solo se tutti i test sono passati, la pipeline deve essere configurata in modo che lo stage di deploy dipenda implicitamente dal successo degli stage precedenti.

deploy_production:
  stage: deploy
  image: alpine:3.20
  script:
    - ./scripts/deploy.sh production
  environment:
    name: production
    url: https://example.com
  only:
    - main
  when: manual
  allow_failure: false

Alcuni punti chiave:

  • when: manual permette un deploy controllato (ad esempio solo da un responsabile tecnico).
  • allow_failure: false impedisce di ignorare eventuali errori nel job di deploy.
  • Se uno qualunque dei job negli stage precedenti fallisce, lo stage di deploy non verrà nemmeno eseguito.

Pipeline per merge request vs pipeline su branch principale

Una strategia comune è differenziare tra:

  • Pipeline per le merge request: eseguono tutti i controlli necessari a decidere se la MR può essere integrata.
  • Pipeline sulla branch principale (es. main): normalmente includono anche i job di build, packaging e deploy verso ambienti di staging/produzione.
workflow:
  rules:
    - if: $CI_MERGE_REQUEST_IID
      when: always
    - if: $CI_COMMIT_BRANCH == "main"
      when: always
    - when: never

In combinazione con le regole only o rules sui singoli job, è possibile eseguire, ad esempio, i test E2E solo sulle merge request critiche o solo quando cambia una certa directory.

Ottimizzazione dei test nella pipeline

1. Cache e artefatti

Per ridurre i tempi di esecuzione, è possibile usare la cache dei pacchetti e degli asset generati:

cache:
  key: "$CI_PROJECT_ID-$CI_COMMIT_REF_SLUG"
  paths:
    - node_modules/
    - .m2/repository

Gli artefatti permettono di condividere risultati tra job (ad esempio report di test e build):

build_app:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/

2. Parallelizzazione

I job nello stesso stage vengono eseguiti in parallelo, se ci sono runner sufficienti. Possiamo dividere test unitari o E2E in più job per accelerare.

unit_tests_part1:
  stage: test
  script:
    - npm test -- --runInBand tests/unit/a*.spec.js

unit_tests_part2:
  stage: test
  script:
    - npm test -- --runInBand tests/unit/b*.spec.js

3. Selezione mirata dei test

Con le regole di GitLab è possibile eseguire solo determinati job in base ai file modificati, riducendo il carico complessivo.

backend_tests:
  stage: test
  script:
    - pytest
  rules:
    - changes:
        - backend/**

Integrazione con quality gate e regole di protezione

La pipeline di test diventa realmente efficace quando è integrata con le regole di protezione dei branch e delle merge request:

  • Richiedere che la pipeline sia verde prima di poter eseguire il merge.
  • Richiedere un certo numero di approvazioni (ad esempio da parte di un reviewer o di un team di QA).
  • Impedire push diretti sulla branch di produzione (ad esempio main o master), consentendo solo merge tramite MR.

In questo modo i test non sono solo un suggerimento, ma un requisito tecnico per far avanzare il codice lungo la pipeline.

Ambienti di staging ed ambienti effimeri

Per test più realistici, è consigliabile introdurre un ambiente di staging o ambienti effimeri (review app) dove eseguire i test E2E e le verifiche manuali:

  • Staging: un ambiente stabile, il più simile possibile alla produzione.
  • Review app: un'istanza creata dinamicamente per ogni merge request.
review_app:
  stage: deploy
  script:
    - ./scripts/deploy_review.sh
  environment:
    name: review/$CI_COMMIT_REF_NAME
    url: https://$CI_COMMIT_REF_SLUG.review.example.com
    on_stop: stop_review_app

stop_review_app:
  stage: deploy
  script:
    - ./scripts/stop_review.sh
  environment:
    name: review/$CI_COMMIT_REF_NAME
    action: stop
  when: manual

Questa strategia permette ai tester e agli stakeholder di validare il comportamento reale dell'applicazione prima che il codice raggiunga la produzione.

Monitoraggio e feedback continuo

Una pipeline di test efficace non si limita a dire "successo" o "fallito". Dovrebbe fornire feedback ricco e facilmente consultabile:

  • Report di test con dettagli su casi falliti e tempi di esecuzione.
  • Storico dei tempi di pipeline per individuare lentezze crescenti.
  • Report di copertura del codice integrati con la UI di GitLab.
coverage_tests:
  stage: test
  script:
    - pytest --cov=app --cov-report=xml
  artifacts:
    reports:
      cobertura: coverage.xml

Buone pratiche riassuntive

  • Mantenere i job di test il più veloci possibile, specialmente quelli eseguiti su ogni commit.
  • Fallire velocemente: evitare di eseguire step costosi se lint e test unitari non sono passati.
  • Automatizzare il più possibile: meno dipendenza da verifiche manuali ripetitive.
  • Versionare la configurazione della pipeline insieme al codice.
  • Rivedere periodicamente i test per eliminare quelli ridondanti o non più utili.

Con una pipeline di test ben progettata in GitLab, il deploy in produzione diventa un'operazione prevedibile e poco rischiosa. Gli sviluppatori possono concentrarsi sulle funzionalità, sapendo che ogni modifica verrà verificata automaticamente prima di raggiungere gli utenti finali.

Torna su