ShellCheck va deixar el toolkit net d'errors de forma, però no sap res del que fa. No et pot dir si veloz_percentatge 0 0 retorna alguna cosa sensata en lloc de petar, si copia.sh respecta de debò els trenta dies de retenció, ni si l'awk amb què vam accelerar l'informe a 08-02 produeix les mateixes xifres que el bucle que va substituir. Això només ho respon executar el codi i comparar el resultat amb l'esperat. Aquesta lliçó respon a la pregunta amb què obria el mòdul —com sé que un canvi no trenca res?— amb Bats, el marc de proves de Bash.
Contingut
- Per què els scripts d'operacions es proven poc, i per què és un error
- Què val la pena provar i què no
- Provar a mà primer
- Instal·lar
bats-corei les seves llibreries - Anatomia d'un fitxer
.bats - Executar les proves
- Aïllar la prova de l'entorn
- Dobles de prova: substituir una ordre externa
- Estructura de
tests/al toolkit - Hook, CI i criteri de cobertura
- Aplicació:
tests/comu.bats
- Per què els scripts d'operacions es proven poc, i per què és un error
Ningú no discuteix que una aplicació web es prova. Amb els scripts de sistema el costum és un altre: s'executen a mà un cop, «funciona», i a producció. Les excuses habituals tenen la seva lògica aparent: «només és un script de 80 línies» —que executa rm -rf com a root cada nit—, «el provo executant-lo» —un cop, amb les dades d'avui, a la teva màquina, amb el teu PATH— i «no hi ha manera de provar una cosa que toca el sistema», que és fals i es resol a l'apartat 8.
L'argument a favor és més fort que tots aquests: els scripts d'operacions fallen de matinada i sense ningú al davant. Una aplicació web caiguda la veu un usuari en deu segons; copia.sh pot portar tres setmanes desant arxius buits i ningú no se n'assabenta fins que cal restaurar. I hi ha un segon argument més pràctic: les proves fan possible refactoritzar. Tot el mòdul 8 consisteix a canviar codi que funciona; sense proves, cada canvi és un acte de fe, i amb elles la refactorització de rendiment de 08-02 es valida en dos segons.
- Què val la pena provar i què no
| Què | Rendibilitat | Per què |
|---|---|---|
Funcions pures de lib/comu.sh (veloz_percentatge, validadors) |
Molt alta | Entrada → sortida, sense efectes; es proven en mil·lisegons |
Funcions amb efectes acotats (veloz_log, escriptura de fitxers) |
Alta | Es comprova el fitxer resultant en un temporal |
| Script complet: codi de sortida, sortida, fitxers que genera | Mitjana-alta | És el que de debò fa servir l'usuari, i on hi ha més bugs silenciosos |
systemctl, ssh a la flota, enviament de correu real |
Baixa | Es proven amb dobles, no de debò |
| Capçaleres, colors del terminal | Nul·la | Es veu a simple vista i canvia constantment |
La regla: prova la lògica, no la lampisteria. Si una funció decideix alguna cosa —un llindar, un format, una validació, una suma—, prova-la. Si només encadena dues ordres del sistema, comprova el conjunt i no les peces.
- Provar a mà primer
Abans d'instal·lar res convé veure què automatitza Bats. Una funció es prova en una sessió interactiva:
$ source ~/veloz-ops/lib/comu.sh
$ veloz_percentatge 412 1284
32.1
$ veloz_percentatge 0 0
veloz_percentatge: el total no pot ser zero # i $? val 1
# El mateix dialeg, escrit com a comprovacio automatica
[[ $(veloz_percentatge 412 1284) == "32.1" ]] || echo "FALLADA: percentatge basic"
veloz_percentatge 0 0 2>/dev/null && echo "FALLADA: hauria de fallar amb total 0"Això ja és una prova: executar amb una entrada coneguda i comparar amb la sortida esperada. Per a tres comprovacions pot ser suficient. El que Bats aporta quan en passes de tres és tot el que aquí falta: noms llegibles per cas, que una fallada no aturi les altres, capturar alhora sortida i codi de sortida, preparar i netejar l'entorn de cada prova i un resum final amb el compte d'encerts i fallades.
- Instal·lar
bats-core i les seves llibreries
bats-core i les seves llibreriesbats-core és el projecte mantingut avui (l'original sstephenson/bats està abandonat).
$ sudo apt install bats # rapid, versio una mica antiga
# O com a submoduls del repositori, que es el recomanable en equip (08-04)
$ git submodule add https://github.com/bats-core/bats-core.git tests/bats
$ git submodule add https://github.com/bats-core/bats-support.git tests/lib/bats-support
$ git submodule add https://github.com/bats-core/bats-assert.git tests/lib/bats-assertLes dues llibreries auxiliars no són imprescindibles, però milloren molt els missatges: bats-support aporta el format d'errors i bats-assert les comprovacions assert_success, assert_failure, assert_output, assert_line i refute_output, que en fallar mostren què esperaven i què han obtingut en comptes d'un escarit «no ha passat».
- Anatomia d'un fitxer
.bats
.batsUn .bats és un script de Bash amb una sintaxi afegida: el bloc @test.
#!/usr/bin/env bats
# tests/comu.bats - proves de la llibreria del toolkit
setup() {
load 'lib/bats-support/load'
load 'lib/bats-assert/load'
source "${BATS_TEST_DIRNAME}/../lib/comu.sh"
}
@test "veloz_percentatge calcula amb un decimal" {
run veloz_percentatge 412 1284
assert_success
assert_output "32.1"
}Les peces, una a una:
@test "descripció" { ... }: cada prova amb un nom que es llegeix a la sortida i que ha de dir quin comportament s'espera, no «prova 1».run ordre ...: executa l'ordre capturant-ne la sortida i el codi de sortida sense que la fallada avorti la prova. És la peça central de Bats.$status: codi de sortida del que ha executatrun.$output: sortida combinada (stdout + stderr).${lines[@]}: la mateixa sortida com a array, una línia per element.setup()s'executa abans de cada prova iteardown()després, fins i tot si falla;setup_file()/teardown_file()un sol cop per fitxer, per a preparatius cars.load fitxercarrega un.bashrelatiu al directori de la prova,skip "motiu"omet una prova iBATS_TEST_DIRNAMEés el directori del.bats, per construir camins independents d'on es llanci.
Sense bats-assert les mateixes comprovacions s'escriuen amb [ "$status" -eq 0 ] i [ "$output" = "32.1" ], cosa que també és vàlida.
- Executar les proves
$ bats tests/
comu.bats
✓ veloz_percentatge calcula amb un decimal
✓ veloz_percentatge falla si el total es zero
✗ veloz_requereix detecta una ordre absent
(in test file tests/comu.bats, line 41)
-- command succeeded, but it was expected to fail --
3 tests, 1 failureOpcions útils: --filter 'percentatge' executa només les proves el nom de les quals casi amb el patró, -t produeix sortida TAP pura —el format estàndard que consumeixen els sistemes de CI—, -x traça cada ordre per depurar i -j 4 executa en paral·lel. El codi de sortida és 0 si tot passa i 1 si alguna cosa falla, que és justament el que necessiten el hook i CI.
- Aïllar la prova de l'entorn
L'error més habitual en començar és escriure proves que depenen de la màquina: del /srv/veloz/dades real, del fus horari, que existeixi jq. Aquestes proves passen al teu portàtil, fallen a CI i acaben desactivades. La solució és que cada prova construeixi el seu propi món en un directori temporal:
setup() {
load 'lib/bats-support/load'
load 'lib/bats-assert/load'
export TZ=UTC LC_ALL=C # res de formats locals
DIR_PROVA="$(mktemp -d "${BATS_TMPDIR}/veloz.XXXXXX")"
export VELOZ_DIR_DADES="$DIR_PROVA/dades" VELOZ_DIR_LOGS="$DIR_PROVA/logs"
mkdir -p "$VELOZ_DIR_DADES" "$VELOZ_DIR_LOGS"
cat > "$VELOZ_DIR_DADES/enviaments.csv" <<'EOF'
id_enviament,data,ciutat,repartidor,estat,import
E000001,2026-08-03,Valencia,alopez,lliurat,24.50
E000002,2026-08-03,Sevilla,mgarcia,incidencia,31.00
E000003,2026-08-03,Valencia,jruiz,en_repartiment,18.75
EOF
source "${BATS_TEST_DIRNAME}/../lib/comu.sh"
}
teardown() { rm -rf "$DIR_PROVA"; }Tres enviaments són suficients: hi ha una ciutat repetida, dos estats i tres repartidors, que és tot el que necessita qualsevol agregació. BATS_TMPDIR és el temporal que gestiona Bats; en versions recents existeix a més BATS_TEST_TMPDIR, creat i esborrat automàticament per prova.
- Dobles de prova: substituir una ordre externa
Aquesta és la tècnica que fa provables els scripts de sistema, i és més simple del que sembla. Bash busca les ordres recorrent $PATH d'esquerra a dreta; si hi poses al principi un directori amb un script anomenat curl, aquest és el curl que s'executa.
setup() {
DIR_PROVA="$(mktemp -d "${BATS_TMPDIR}/veloz.XXXXXX")"
mkdir -p "$DIR_PROVA/bin"
cat > "$DIR_PROVA/bin/curl" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$REGISTRE_CRIDES" # anota com se l'ha cridat
printf '{"estat":"ok","enviaments":128}\n'
EOF
chmod +x "$DIR_PROVA/bin/curl"
export REGISTRE_CRIDES="$DIR_PROVA/crides.txt"
export PATH="$DIR_PROVA/bin:$PATH" # el doble te prioritat
source "${BATS_TEST_DIRNAME}/../lib/comu.sh"
}
@test "veloz_api_get retorna el JSON de l'API" {
run veloz_api_get /metricas
assert_success
assert_output --partial '"enviaments":128'
}
@test "veloz_api_get fa servir --netrc i no credencials a la linia d'ordres" {
veloz_api_get /salud >/dev/null
run grep -c -- '--netrc' "$REGISTRE_CRIDES"
assert_output "1"
}El doble aconsegueix tres coses, i només la primera és «no fer servir la xarxa». Determinisme: la resposta és sempre la mateixa, així que la prova no falla perquè l'API vagi lenta. Casos impossibles de provocar: canviant el doble perquè retorni exit 7 o un JSON truncat es prova la gestió d'errors, que és justament el codi que mai no s'exercita a mà. I verificació de la crida: el registre permet comprovar com s'ha invocat, que és com s'ha provat més amunt l'ús de --netrc de 08-03.
La mateixa tècnica cobreix la resta: un doble de systemctl que retorni inactive prova la branca d'alerta d'estat-servei.sh; un d'ssh prova flota.sh sense sortir de la màquina; un de mail verifica que vigilant.sh avisa sense enviar res a ningú. I per a copia.sh no cal doble: n'hi ha prou d'apuntar VELOZ_DIR_DADES i VELOZ_DIR_COPIA al temporal perquè treballi sobre quatre fitxers ficticis en lloc de sobre el disc real.
- Estructura de
tests/ al toolkit
tests/ al toolkitEl directori tests/ conté els submòduls (bats/, lib/) i un .bats per unitat provada: comu.bats per a les funcions de la llibreria, informe.bats per a informe-diari.sh d'extrem a extrem i copia.bats per a la retenció i la restauració.
| Fitxer | Casos normals | Casos límit | Casos d'error |
|---|---|---|---|
comu.bats |
veloz_percentatge 412 1284 → 32.1 |
numerador 0, total 0, valor més gran que el total | arguments no numèrics → codi 1 |
informe.bats |
CSV de 4 files → totals per ciutat | CSV només amb capçalera, camp buit | CSV inexistent → codi 2 i missatge |
copia.bats |
crea el .tar.gz i el seu .sha256 |
0 fitxers; nom amb espais | destí sense permisos → codi ≠ 0 |
Els casos límit són els que més bugs troben: un CSV només amb la capçalera fa que molts informes divideixin entre zero, un import 1.234,50 trenca la suma i un fitxer amb espais desmunta el find mal escrit. I provar el codi de sortida no és un detall: és l'única cosa que veu el timer de systemd de 07-05 i el que decideix si s'envia una alerta.
- Hook, CI i criteri de cobertura
El hook pre-commit de 08-04 ja invocava bats tests/. A CI s'afegeix al costat de l'anàlisi estàtica, amb bats --formatter tap tests/. Un detall important perquè això sigui sostenible: les proves han de ser ràpides. Si triguen quaranta segons, el hook es torna molest i algú començarà a fer servir --no-verify; amb dobles i dades fictícies, la suite completa del toolkit triga menys de dos segons, i això és el que garanteix que s'executi sempre.
No cal provar-ho tot. El criteri, per ordre de prioritat: el que ja s'ha trencat alguna vegada —cada incident resolt es converteix en una prova, i aquesta és la regla més valuosa, perquè garanteix que la mateixa fallada no torna i fa créixer la suite justament per on el sistema és fràgil—; el que decideix alguna cosa (llindars, validacions, càlculs); el que és destructiu (retenció, esborrat, sobreescriptura), perquè una fallada aquí no es desfà; i els codis de sortida dels scripts que governen alertes i timers. Perseguir el 100 % de cobertura en Bash produeix proves fràgils que es trenquen amb cada canvi de format i acaben desactivades, cosa pitjor que no tenir-les.
- Aplicació:
tests/comu.bats
tests/comu.bats#!/usr/bin/env bats
# tests/comu.bats - proves de lib/comu.sh del toolkit de Veloz Envios
# setup() i teardown() com a l'apartat 7, amb VELOZ_DIR_LOGS al temporal
@test "percentatge: cas normal amb un decimal" {
run veloz_percentatge 412 1284
assert_success
assert_output "32.1"
}
@test "percentatge: numerador zero retorna 0.0" {
run veloz_percentatge 0 1284
assert_success
assert_output "0.0"
}
@test "percentatge: total zero falla i no divideix" {
run veloz_percentatge 5 0
assert_failure
assert_output --partial "total"
}
@test "requereix: falla i anomena l'ordre absent" {
run veloz_requereix ordre_que_no_existeix_12345
assert_failure
assert_output --partial "ordre_que_no_existeix_12345"
}
@test "log: escriu nivell, missatge i marca de temps ISO-8601" {
veloz_log INFO "copia completada"
run cat "$VELOZ_DIR_LOGS/veloz-ops.log"
assert_output --partial "[INFO] copia completada"
assert_line --regexp '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}'
}
@test "log: no interpreta el contingut del missatge" {
veloz_log INFO 'perill $(id) i *'
run cat "$VELOZ_DIR_LOGS/veloz-ops.log"
assert_output --partial 'perill $(id) i *'
}Cinc proves, menys de dos segons, i cobreixen els tres tipus de cas (a les quals convé afegir el cas positiu veloz_requereix bash). L'última mereix atenció: comprova que veloz_log tracta el seu argument com a text i no l'expandeix, que és exactament la vulnerabilitat d'injecció de 08-03. Una prova pot vigilar una propietat de seguretat igual que vigila un càlcul.
Errors Habituals i Consells
- Oblidar
run. Sense ell, una ordre que falla avorta la prova i no pots comprovar$status. - Proves que depenen de la màquina. Camins reals, fus horari,
jqinstal·lat. Aïlla-ho tot asetupamb temporal,TZiLC_ALL. - No netejar al
teardown. Les proves es contaminen entre si i fallen segons l'ordre d'execució. - Provar només el camí feliç. Els bugs viuen al CSV buit, al camp absent i al disc ple.
- Consell: converteix cada incident en una prova. És la manera més barata que la suite cobreixi justament el que importa.
- Consell:
bats -x --filter 'nom'executa una sola prova amb traça completa; és elset -xde 05-03 aplicat a les proves.
Exercicis
Exercici 1. Escriu una prova Bats que verifiqui que informe-diari.sh falla amb codi 2 i un missatge a stderr quan el CSV d'entrada no existeix.
Exercici 2. Explica per què aquesta prova està malament i reescriu-la.
Solucions
Solució 1.
@test "informe: CSV inexistent falla amb codi 2 i missatge" {
run "${BATS_TEST_DIRNAME}/../bin/informe-diari.sh" -f "$DIR_PROVA/no-existeix.csv"
assert_failure 2
assert_output --partial "no-existeix.csv"
}assert_failure 2 exigeix el codi exacte, no una fallada qualsevol: és el que distingeix «error d'ús» d'«error intern» segons els codis convencionals de 05-03. Com que run captura stdout i stderr junts, assert_output veu el missatge encara que s'escrigui a stderr.
Solució 2. Té quatre problemes: fa servir un camí absolut de l'usuari, així que només funciona en aquella màquina; executa la còpia sobre les dades reals, amb risc de sobreescriure la còpia de producció des d'una prova; només comprova el codi de sortida, no que la còpia contingui res; i el seu nom no diu quin comportament s'espera.
@test "copia: crea el tar.gz i la seva suma de comprovacio" {
export VELOZ_DIR_DADES="$DIR_PROVA/dades" VELOZ_DIR_COPIA="$DIR_PROVA/copia"
mkdir -p "$VELOZ_DIR_DADES" "$VELOZ_DIR_COPIA"
printf 'contingut\n' > "$VELOZ_DIR_DADES/enviaments.csv"
run "${BATS_TEST_DIRNAME}/../bin/copia.sh"
assert_success
run bash -c 'ls "$VELOZ_DIR_COPIA"/*.tar.gz "$VELOZ_DIR_COPIA"/*.sha256'
assert_success
run tar -tzf "$(ls "$VELOZ_DIR_COPIA"/*.tar.gz)"
assert_output --partial "enviaments.csv"
}Ara la prova és independent de la màquina, treballa sobre dades fictícies i verifica l'efecte: que l'arxiu existeix, que existeix la seva suma de comprovació (07-03) i que a dins hi ha el fitxer esperat.
Conclusió
Provar scripts d'operacions no és un luxe: és l'única detecció primerenca disponible en un codi que s'executa de matinada i sense ningú al davant, on una còpia buida pot passar setmanes inadvertida. El més rendible són les funcions pures de lib/comu.sh, seguides del comportament d'extrem a extrem de cada script —codi de sortida, sortida i fitxers que crea—; la lampisteria sense lògica no es prova. La idea és la mateixa que la comprovació casolana [[ $(f x) == esperat ]] || echo FALLADA, i Bats hi afegeix el que a aquella li falta: noms llegibles, aïllament entre casos, resum final i, sobretot, run, que captura $status, $output i ${lines[@]} sense que una fallada avorti la prova. Amb setup/teardown cada cas construeix el seu món en un directori temporal amb dades fictícies de Veloz Envíos i fixa TZ i LC_ALL, de manera que la prova no depengui de la màquina —el motiu principal pel qual les suites acaben desactivades—. Els dobles de prova són la tècnica que ho fa tot possible: un script fals anomenat curl, systemctl, ssh o mail en un directori al principi del PATH permet provar veloz_api_get sense xarxa, flota.sh sense servidors i copia.sh sense tocar el disc, a més de provocar els errors que mai no veuries a mà i de verificar com s'ha invocat l'ordre. La suite s'organitza en tests/comu.bats, tests/informe.bats i tests/copia.bats, cobrint en cadascuna casos normals, límit i d'error amb els seus codis de sortida, i s'executa al hook pre-commit i a CI (08-04, 08-05), cosa que exigeix que sigui ràpida: amb dobles, menys de dos segons. I el criteri de cobertura no és un percentatge, sinó una prioritat: el que ja s'ha trencat un cop, el que decideix alguna cosa i el que és destructiu.
Ja tenim un toolkit llegible, ràpid, auditat, versionat, analitzat i provat. Falta una decisió que hem donat per suposada tot el curs: que l'intèrpret és Bash 5 sobre Ubuntu. La lliçó 08-07 la posa a prova: què passa quan un script s'ha d'executar allà on /bin/sh és dash, ash d'Alpine o ksh, quines construccions de les que fem servir són bashismes i quin és el seu equivalent POSIX, per què les diferències entre les eines GNU i BSD fan més mal que les del mateix shell, com comprovar la portabilitat amb checkbashisms, shellcheck -s sh i dash -n, i quan POSIX pur val la pena i quan és un llast. Amb aquesta lliçó es tanca el mòdul sencer.
Curs de Programació en Bash
Mòdul 1: Introducció a Bash
- Què és Bash?
- Configurar el teu Entorn
- Navegació Bàsica per la Línia d'Ordres
- Entendre el Shell
- Trobar Ajuda: man, help i --help
Mòdul 2: Ordres Bàsiques de Bash
- Operacions amb Fitxers i Directoris
- Ordres de Processament de Text
- Permisos i Propietat dels Fitxers
- Redirecció i Canonades
- Comodins i Expansió de Rutes
- Historial i Dreceres de Teclat
Mòdul 3: Fonaments de Scripting
- Crear i Executar un Script
- Variables i Constants
- Operadors Bàsics
- Sentències Condicionals
- Arguments i Entrada de l'Usuari
- Cometes, Expansió i Substitució
Mòdul 4: Scripting Intermedi
- Bucles en Bash
- Funcions en Bash
- Arrays i Arrays Associatius
- Manipulació de Cadenes
- La Sentència case i els Menús Interactius
- Aritmètica i Càlculs Numèrics
Mòdul 5: Tècniques Avançades de Scripting
- Operacions Avançades amb Fitxers
- Gestió de Processos
- Gestió d'Errors i Depuració
- Expressions Regulars
- Entrada/Sortida Avançada: Descriptors i Here-Documents
- Scripts Modulars i Llibreries Reutilitzables
Mòdul 6: Treballar amb Eines Externes
Mòdul 7: Automatització i Programació
- Tasques Cron
- Automatitzar Tasques
- Scripts de Còpia i Restauració
- Monitoratge i Registre
- Serveis i Temporitzadors amb systemd
- Automatització Remota amb SSH
Mòdul 8: Bones Pràctiques i Optimització
- Escriure Codi Llegible
- Optimitzar Scripts en Bash
- Consideracions de Seguretat
- Control de Versions amb Git
- Anàlisi Estàtica amb ShellCheck i shfmt
- Proves Automatitzades amb Bats
- Portabilitat: POSIX sh enfront de Bashismes
