Lezione 27 di 35

Git 27

🔀 Git & GitHub · Modulo 5 · Lezione 27

CI: test automatici 🧪

Scrivere test · Integrarli nel workflow · Bloccare il merge se falliscono · Test per HTML, JS e Python

⏱️ Durata stimata: 75–90 min 📊 Livello: Avanzato (Modulo 5) 🔧 Strumenti: Terminale · Editor · GitHub 📋 Prerequisiti: Lezioni 25–26 (workflow e YAML)

🧪 Un workflow CI senza test veri è un semaforo decorativo. Nella Lezione 25 hai verificato che i file esistessero — utile, ma un vero CI esegue test del codice: la funzione restituisce il valore corretto? Il form gestisce gli errori? L’API risponde con lo status giusto? In questa lezione scrivi test reali e li integri nel workflow. Quando un test fallisce, la PR si blocca — nessun codice rotto entra in main.

🎯 Obiettivi della lezione

  • Capire cos’è un test automatico e perché è fondamentale nel CI
  • Scrivere test semplici in JavaScript (con Node.js assert) e Python (con pytest)
  • Creare un workflow CI che esegue i test a ogni push e PR
  • Collegare il CI alle branch protection rules per bloccare il merge
  • Leggere e interpretare i risultati dei test nei log di Actions

📖 1. Teoria

1.1 — Cos’è un test automatico

Un test automatico è un programma che verifica che il tuo codice faccia quello che deve fare. Lo scrivi una volta, e da quel momento viene eseguito automaticamente a ogni push — centinaia, migliaia di volte. Se qualcuno rompe qualcosa, il test fallisce e il CI segnala il problema.

❌ Test manuale
“Apro il sito, clicco i pulsanti, sembra funzionare.” Richiede tempo, è soggettivo, non ripetibile, e dimentichi di controllare i casi limite. Dopo 50 modifiche, non testi più nulla.
✅ Test automatico
Un programma che chiama la tua funzione con input noti e verifica che l’output sia quello atteso. Eseguito in millisecondi, ripetibile all’infinito, obiettivo. Trova il bug prima che arrivi in produzione.
L’anatomia di un test
1. Arrange
Prepara i dati di input. Cosa dai alla funzione?
2. Act
Esegui la funzione. Chiama il codice che vuoi testare.
3. Assert
Verifica il risultato. L’output è quello che ti aspettavi?

1.2 — Test in JavaScript con Node.js

Node.js ha un modulo assert integrato e un test runner nativo (da Node 18). Non servono librerie esterne per iniziare.

// File: utils.js — il codice da testare

function somma(a, b) {
return a + b;
}

function capitalizza(str) {
if (!str) return “”;
return str[0].toUpperCase() + str.slice(1);
}

function isPari(n) {
return n % 2 === 0;
}

module.exports = { somma, capitalizza, isPari };
// File: utils.test.js — i test

const assert = require(“assert”);
const { somma, capitalizza, isPari } = require(“./utils”);

// Test somma
assert.strictEqual(somma(2, 3), 5, “2+3 deve fare 5”);
assert.strictEqual(somma(0, 0), 0, “0+0 deve fare 0”);
assert.strictEqual(somma(-1, 1), 0, “-1+1 deve fare 0”);

// Test capitalizza
assert.strictEqual(capitalizza(“ciao”), “Ciao”);
assert.strictEqual(capitalizza(“”), “”);
assert.strictEqual(capitalizza(“a”), “A”);

// Test isPari
assert.strictEqual(isPari(4), true);
assert.strictEqual(isPari(7), false);
assert.strictEqual(isPari(0), true);

console.log(“✅ Tutti i 9 test passati!”);
# Eseguire i test in locale
$ node utils.test.js
✅ Tutti i 9 test passati!

# Se un test fallisce:
$ node utils.test.js
AssertionError: 2+3 deve fare 5
actual: 6
expected: 5
# Il processo esce con codice 1 → il CI lo vede come fallimento

1.3 — Test in Python con pytest

# File: utils.py — il codice da testare

def somma(a, b):
return a + b

def capitalizza(s):
if not s:
return “”
return s[0].upper() + s[1:]

def is_pari(n):
return n % 2 == 0
# File: test_utils.py — i test (pytest cerca file che iniziano con test_)

from utils import somma, capitalizza, is_pari

def test_somma():
assert somma(2, 3) == 5
assert somma(0, 0) == 0
assert somma(-1, 1) == 0

def test_capitalizza():
assert capitalizza(“ciao”) == “Ciao”
assert capitalizza(“”) == “”
assert capitalizza(“a”) == “A”

def test_is_pari():
assert is_pari(4) == True
assert is_pari(7) == False
assert is_pari(0) == True
# Eseguire i test in locale
$ pip install pytest
$ pytest
=================== test session starts ====================
collected 3 items

test_utils.py … [100%]

==================== 3 passed in 0.01s ====================

1.4 — Il workflow CI con test

Ora colleghi i test al CI: ogni push e ogni PR eseguono automaticamente i test. Se falliscono, il workflow è rosso.

CI per JavaScript
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      – uses: actions/checkout@v4
      – uses: actions/setup-node@v4
        with:
          node-version: 20
      – name: Esegui test
        run: node utils.test.js
CI per Python
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      – uses: actions/checkout@v4
      – uses: actions/setup-python@v5
        with:
          python-version: “3.12”
      – run: pip install pytest
      – name: Esegui test
        run: pytest -v

1.5 — Collegare il CI alle branch protection rules

Il vero potere del CI emerge quando lo colleghi alle branch protection (Lezione 20). Il risultato: se i test falliscono, il merge è bloccato.

Come configurare: require status checks
1
Assicurati che il workflow CI funzioni (almeno un’esecuzione completata).
2
Settings → Rules → Rulesets → modifica il ruleset di main (o creane uno).
3
Attiva “Require status checks to pass”. Nella casella di ricerca, cerca il nome del tuo job (es. “test”) e selezionalo.
4
Salva. Da ora: se il job “test” fallisce, il pulsante Merge resta grigio.
Il flusso con CI + branch protection
1. PUSH / PR
Il codice arriva su GitHub
2. CI AUTOMATICO
Actions esegue i test
3. CHECK ✅ / ❌
Risultato nella PR
4. MERGE O BLOCCO
✅ → merge possibile
❌ → merge bloccato

1.6 — Cosa testare: la piramide dei test

E2E (pochi)
Test del sito intero nel browser
Integrazione (alcuni)
Test di più componenti insieme
Unit test (tanti) ⭐
Test di singole funzioni — veloci, semplici, affidabili
Parti dal basso: tanti unit test veloci, pochi test E2E lenti. Per la scuola, gli unit test bastano.

📌 Cosa testare nei progetti scolastici: le funzioni di logica (calcoli, validazioni, trasformazioni), i casi limite (input vuoto, zero, numeri negativi) e le condizioni di errore (cosa succede se l’input è sbagliato?). Non serve testare HTML statico o CSS — testa il codice che fa qualcosa.

1.7 — Test per progetti senza framework: script di verifica

Se il tuo progetto è un sito HTML/CSS senza JavaScript logico, puoi comunque avere test utili — verifiche sulla struttura e la qualità del progetto:

# File: test.sh — script di verifica per progetto HTML
#!/bin/bash
ERRORI=0

echo “🧪 Esecuzione test del progetto…”

# Test 1: file essenziali
for file in index.html style.css README.md; do
if [ ! -f “$file” ]; then
echo “❌ FAIL: $file mancante”
ERRORI=$((ERRORI + 1))
else
echo “✅ PASS: $file presente”
fi
done

# Test 2: HTML ha DOCTYPE
for f in $(find . -name “*.html”); do
if ! grep -qi “DOCTYPE” “$f”; then
echo “❌ FAIL: $f manca DOCTYPE”
ERRORI=$((ERRORI + 1))
else
echo “✅ PASS: $f ha DOCTYPE”
fi
done

# Test 3: niente credenziali nel codice
if grep -r “password\|api_key\|secret” –include=”*.js” –include=”*.html” . 2>/dev/null; then
echo “❌ FAIL: possibili credenziali nel codice!”
ERRORI=$((ERRORI + 1))
else
echo “✅ PASS: nessuna credenziale trovata”
fi

# Test 4: .gitignore esiste
if [ ! -f “.gitignore” ]; then
echo “❌ FAIL: .gitignore mancante”
ERRORI=$((ERRORI + 1))
else
echo “✅ PASS: .gitignore presente”
fi

# Risultato finale
echo “”
if [ $ERRORI -gt 0 ]; then
echo “💥 $ERRORI test falliti!”
exit 1
else
echo “🎉 Tutti i test passati!”
exit 0
fi
# Workflow che usa lo script
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4
– name: Esegui test
run: bash test.sh

💻 2. Esempi guidati

Esempio A — CI JavaScript completo

# 1. Crea i file
$ cat > utils.js <<‘EOF’
function somma(a, b) { return a + b; }
function isPari(n) { return n % 2 === 0; }
module.exports = { somma, isPari };
EOF

$ cat > utils.test.js <<‘EOF’
const assert = require(“assert”);
const { somma, isPari } = require(“./utils”);
assert.strictEqual(somma(2, 3), 5);
assert.strictEqual(somma(-1, 1), 0);
assert.strictEqual(isPari(4), true);
assert.strictEqual(isPari(7), false);
console.log(“✅ 4 test passati!”);
EOF

# 2. Testa in locale
$ node utils.test.js
✅ 4 test passati!

# 3. Crea il workflow CI
$ mkdir -p .github/workflows
$ cat > .github/workflows/ci.yml <<‘EOF’
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4
– uses: actions/setup-node@v4
with:
node-version: 20
– run: node utils.test.js
EOF

# 4. Committa e pusha
$ git add . && git commit -m “Aggiunti test e CI workflow”
$ git push
# Vai su Actions → il workflow è verde!

Esempio B — Far fallire intenzionalmente un test

# Introduci un bug nella funzione somma
$ sed -i ‘s/return a + b/return a * b/’ utils.js ← somma ora fa moltiplicazione!
$ node utils.test.js
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal.
actual: 6
expected: 5

# Committa e pusha il bug
$ git add . && git commit -m “WIP: modifica somma”
$ git push
# Su Actions: il workflow è ROSSO ❌
# Se è una PR: il check è rosso, il merge è bloccato.

# Correggi il bug
$ sed -i ‘s/return a \* b/return a + b/’ utils.js
$ git add . && git commit -m “Fix: corretto bug in somma”
$ git push
# Actions: torna VERDE ✅ — merge sbloccato!

🏋️ 3. Esercizi

🟢 Esercizio 1 — Primo test CI (Facile)

a) Segui l’Esempio A: crea utils.js, utils.test.js e il workflow CI.
b) Verifica che i test passino in locale: node utils.test.js.
c) Committa e pusha. Il workflow è verde su Actions?
d) Introduci un bug (come nell’Esempio B). Pusha. Il workflow diventa rosso?
e) Correggi e pusha. Torna verde?

🔵 Esercizio 2 — Più funzioni, più test (Medio)

a) Aggiungi a utils.js 3 nuove funzioni: moltiplica(a, b), invertiStringa(str), contaVocali(str).
b) Scrivi almeno 3 test per ogni funzione (includi casi limite: stringa vuota, zero, numeri negativi).
c) Esegui in locale: tutti passano?
d) Committa, pusha, verifica il CI.
e) Quanti test hai in totale? Il messaggio di successo li conta?

🟠 Esercizio 3 — CI + branch protection (Medio)

a) Con il CI funzionante, configura “Require status checks to pass” nel ruleset di main (sezione 1.5).
b) Crea un branch, introduci un bug che fa fallire i test, pusha, apri una PR.
c) Nella PR: il check è rosso? Il pulsante Merge è grigio?
d) Correggi il bug, pusha. Il check diventa verde? Il merge è sbloccato?
e) Fai merge. Hai appena sperimentato il CI reale: codice rotto non entra in main.

🟣 Esercizio 4 — Suite di test per il tuo progetto (Difficile)

Scegli il linguaggio del tuo progetto (JS o Python) e crea una suite di test completa:

a) Se JS: crea un file test.sh con verifiche strutturali (file esistenti, DOCTYPE, no credenziali) + un file *.test.js con test delle funzioni JS del progetto. Il workflow esegue entrambi.
b) Se Python: crea file test_*.py con pytest. Testa tutte le funzioni del tuo progetto. Il workflow installa le dipendenze e esegue pytest -v.
c) Raggiungi almeno 15 test in totale.
d) Il workflow deve avere 2 job: struttura (verifiche file) e logica (test del codice), in parallelo.
e) Configura branch protection con status check obbligatorio su entrambi i job.
f) Fai una PR con un bug intenzionale: il CI la blocca? Correggi e verifica.

❓ 4. Quiz di verifica

1. Cos’è un test automatico e perché è meglio di un test manuale?

Mostra risposta

Un test automatico è un programma che verifica il comportamento del tuo codice — chiama una funzione con input noti e controlla che l’output sia quello atteso. È meglio del test manuale perché è ripetibile (si esegue identico ogni volta), veloce (millisecondi vs minuti), obiettivo (non dipende dall’attenzione di chi testa), e scalabile (puoi avere centinaia di test che girano a ogni push). Un test manuale (“apro il sito e clicco”) è soggettivo, lento, non ripetibile e dopo 50 modifiche non lo fai più.


2. Come fa il CI a sapere che un test è fallito?

Mostra risposta

Tramite l’exit code del processo. Quando un test fallisce, il programma di test (Node.js, pytest, lo script bash) esce con codice diverso da 0 (tipicamente 1). GitHub Actions interpreta qualsiasi exit code non-zero come un fallimento dello step, che a sua volta fa fallire il job e il workflow. Non serve nessuna configurazione speciale — se il comando run: esce con errore, lo step è rosso. Per questo lo script bash usa exit 1 in caso di errore e exit 0 in caso di successo.


3. Cosa sono le 3 fasi di un test (Arrange, Act, Assert)?

Mostra risposta

Arrange: prepara i dati di input e le condizioni iniziali. Act: esegui il codice che vuoi testare (chiama la funzione). Assert: verifica che il risultato sia quello che ti aspettavi. Esempio: Arrange = “preparo a=2, b=3”; Act = “chiamo somma(2, 3)”; Assert = “verifico che il risultato sia 5”. In JavaScript con assert: assert.strictEqual(somma(2, 3), 5) racchiude tutte e 3 le fasi in una riga. In pytest: assert somma(2, 3) == 5. Il pattern AAA rende i test chiari e leggibili.


4. Come si blocca il merge se i test falliscono?

Mostra risposta

Configurando “Require status checks to pass” nelle branch protection rules (o rulesets) di main. Vai su Settings → Rules → Rulesets → modifica il ruleset → attiva “Require status checks to pass” → cerca e seleziona il nome del job del tuo workflow (es. “test”). Da quel momento, il pulsante Merge nella PR resta grigio finché il job “test” non è verde. Se i test falliscono, il merge è fisicamente impossibile. È la combinazione di CI (test automatici) + branch protection (regole) che garantisce che codice rotto non entri in main.


5. Se il tuo progetto è solo HTML e CSS senza JavaScript, ha senso avere un CI?

Mostra risposta

Sì. Anche senza codice logico, puoi avere test utili: verificare che i file essenziali esistano, che ogni file HTML abbia DOCTYPE e tag di chiusura, che non ci siano credenziali nel codice, che il .gitignore sia presente, che le immagini referenziate esistano, che il README contenga le sezioni richieste. Uno script bash (come nella sezione 1.7) può fare tutte queste verifiche. Non sono unit test nel senso classico, ma sono controlli di qualità automatici che prevengono errori comuni e mantengono standard nel progetto.


6. Cos’è la “piramide dei test”?

Mostra risposta

È un modello che descrive quanti test di ciascun tipo dovresti avere. Alla base ci sono gli unit test (tanti — veloci, semplici, testano singole funzioni), al centro i test di integrazione (alcuni — testano più componenti insieme), in cima i test end-to-end (pochi — testano l’intera applicazione, sono lenti e fragili). La forma a piramide indica che la maggior parte dei test dovrebbe essere di tipo unit — sono i più veloci, economici e affidabili. Per i progetti scolastici, gli unit test alla base della piramide sono più che sufficienti.


📚 Riepilogo della lezione

Cosa hai imparato oggi:
  • Test automatici: Arrange → Act → Assert — ripetibili, veloci, obiettivi
  • JavaScript: assert.strictEqual() con node file.test.js
  • Python: assert con pytest -v
  • Script bash: verifiche strutturali per progetti HTML/CSS senza JS
  • Workflow CI: setup-node o setup-python → installa → testa
  • CI + branch protection: “Require status checks to pass” → merge bloccato se test falliscono
  • Piramide: tanti unit test (base) → alcuni integrazione → pochi E2E (cima)
  • Exit code: exit 1 = fallimento → step rosso → workflow rosso → merge bloccato

Prossima lezione →

Lezione 28 — CD: deploy automatico
Pubblicare il sito su GitHub Pages automaticamente dopo il merge.
Deploy continuo: da commit a sito online in un minuto.

Corso Git & GitHub · Per studenti e insegnanti · Lezione 27 di 45