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
🧪 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.
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.
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 };
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!”);
$ 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
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
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
$ 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.
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
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.
❌ → merge bloccato
1.6 — Cosa testare: la piramide dei test
📌 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:
#!/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
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
$ 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
$ 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)
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)
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)
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)
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
- Test automatici: Arrange → Act → Assert — ripetibili, veloci, obiettivi
- JavaScript:
assert.strictEqual()connode file.test.js - Python:
assertconpytest -v - Script bash: verifiche strutturali per progetti HTML/CSS senza JS
- Workflow CI:
setup-nodeosetup-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