Skip to content

La filosofia di ingegneria di Red Sift: testare in fretta, testare a fondo

Il team di ingegneria di Red Sift racconta il nostro approccio al testing del software: cosa significano velocità e rigore nella pratica e perché rendono il prodotto più affidabile.

Red Sift
Published: April 30, 2019·Updated: March 18, 2024·13 min read

Una versione di questo blog è stata originariamente pubblicata da Test MagazineTest Magazine nel marzo 2019, la condividiamo di nuovo per chi se la fosse persa la prima volta!

Developers like flame wars. Last Friday, when the afternoon was slowing in the office, an inadvertent GIF on Slack sparked some friendly debate about the right way to test. Because winning flame wars is important, here is my “well, actually”… 

A un certo punto negli anni 2000, quando PHP non era più considerato solo un linguaggio di templating e Ruby aveva appena iniziato ad appoggiarsi a Rails, la comunità dei programmatori decise che i linguaggi a tipizzazione dinamica fossero un ottimo modo per ridurre il carico cognitivo del programmatore, rimanere spietatamente pragmatici ed evitare la cultura da fabbrica di Java.

Il mantra di Facebook “Move fast and break things” da allora si è diffuso in tutta la scena delle startup, e viene considerato il modo per costruire business, prodotti, case, o praticamente qualsiasi cosa che possa crollarti addosso. E se avete mai lavorato sul software, sapete che ciò che può accadere, accadrà.

Nel frattempo è arrivato Clean Code, e l'ascesa del TDD ha raccolto una folla attorno al principio di Scrivere Prima i Test, alla fede nella Coverage, e ad altri mantra divertenti da recitare, ma molto meno divertenti da metttere in pratica.

Molti libri di auto-aiuto come Clean Code si basano vagamente su esperienze personali, e portano determinati pattern di programmazione in ambiti dove tradizionalmente non venivano usati: per esempio, semplificare l'orientamento agli oggetti in stile C++ con alcuni concetti di programmazione funzionale come funzioni piccole, semplici e pure.

In realtà, esiste molta ricerca sulla crisi del software e su come uscire dal pasticcio in cui ci troviamo, e spesso contraddice la saggezza popolare. Diamo un'occhiata a diverse strategie che guidano la qualità del software, e a dove fanno davvero la differenza.

Dal basso verso l'alto, io generalmente considero la verifica del software ai seguenti livelli: sistema di tipi, unit test, test di integrazione, e struttura organizzativa di gestione.

Aspetta, cosa? Struttura organizzativa di gestione? Beh, forse possiamo iniziare proprio da lì.

Gestione

Il comportamento organizzativo è una scienza sociale a sé stante, e studia la sottile arte delle strutture di gestione delle persone. Dato che il software viene solitamente realizzato da esseri umani, questi hanno bisogni, motivatori interni ed esterni, e occasionalmente devono lavorare insieme per consegnare qualche prodotto finale.

“quantifiable results from the study show that team and collaboration structure can be a better predictor of quality than tooling, testing strategies, or other code-based metrics.” 

Google ha condotto uno studio sui propri team come unità di lavoro per identificare cosa li rendesse più efficaci, mentre la ricerca di Microsoft si è concentrata su come la struttura organizzativa determinasse i tassi di fallimento del software. Entrambi sono approcci interessanti a modo loro, e lo studio di Microsoft ci offre una visione interessante sullo sviluppo di Windows Vista. Lo studio ci dice che team più piccoli e più focalizzati producono software più affidabile. Un alto turnover di ingegneri riduce la qualità del software, mentre una collaborazione più stretta tra team che lavorano sullo stesso progetto porta a tassi di fallimento più bassi.

Queste potrebbero sembrare affermazioni provenienti dal Capitano Ovvio (o da Melvin Conway), tuttavia, i risultati quantificabili dello studio mostrano che la struttura del team e della collaborazione può essere un predittore migliore della qualità rispetto agli strumenti, alle strategie di testing, o ad altre metriche basate sul codice.

Il modo in cui ogni singolo team controlla la qualità del proprio output è il passo successivo. I code review in particolare sono un ottimo modo per creare e mantenere un insieme comune di standard. I code review scritti costringono gli ingegneri a comunicare chiaramente le proprie preoccupazioni, e questa maggiore comunicazione tecnica aiuterà tutti nel team a conoscere stili e prospettive diverse, contribuendo al contempo a livellare le competenze all'interno del team. Magari evitando, però, lo stile di Linus Torvalds.

Test di integrazione

I test di integrazione, a sorpresa, testano l'integrazione di componenti o moduli in un sistema. Si può anche testare l'integrazione di moduli integrati, e ci sono tartarughe fino in fondo.

Spesso è più facile scrivere codice corretto in isolamento, quindi un gran numero di bug si verifica ai confini del sistema. Validazione degli input e formattazione degli output, mancata verifica dei livelli di permesso, o cattiva implementazione degli schemi di interfaccia. Questo problema è amplificato dall'attuale tendenza dei microservizi, dove le versioni delle interfacce possono disallinearsi tra i vari servizi all'interno del sistema.

A questo livello, la cosa migliore è scrivere test end-to-end pass-through per le funzionalità, e cercare di sfruttare il fatto che abbiamo così tanti altri livelli di protezione contro i fallimenti che qualcosa alla fine farà scattare quei meccanismi. Infatti, è stato dimostrato che la copertura del codice nei test di integrazione non è un indicatore affidabile dei tassi di fallimento.

Se la guardi da un'altra prospettiva, la produzione è solo un grande test di integrazione. Un trucco che amo fare è creare un'istanza mute-production, che riceve una parte del traffico effettivo di produzione, ma non genererà mai risposte agli utenti. Con un investimento sufficiente in un livello di orchestrazione stateless, possiamo persino mute-testare sottoalberi di servizi in punti strategici, poi renderli attivi e scartare il vecchio sottoalbero una volta che il carico di lavoro è terminato.

Combinato con i principi alla base della costruzione di sistemi altamente osservabili, questo tipo di ambiente di test elimina gran parte dell'ansia su cosa succede quando distribuiamo in produzione, perché il mute-prod riceverà esattamente i medesimi dati. Più manopole e sonde esponiamo nei sistemi live, migliore visibilità otteniamo sugli aspetti interni.

Unit testing

Quindi, per integrare i moduli, vogliamo avere una certa sicurezza che i moduli stessi funzionino secondo le specifiche. Ed è qui che entra in gioco l'unit testing.

Gli unit test sono solitamente rapidi, e test più o meno completi di pezzi isolati e facili da comprendere. Quanto rapidi? Il repository master di Ruby on Rails esegue circa 67 test e 176 asserzioni per secondo.

Come regola generale, un test dovrebbe coprire uno scenario che può accadere a un modulo. In confronto al testing di integrazione, lo stesso studio di Niedermayr, Juergens e Wagner mostra che la copertura del codice a livello di unit testing influenza davvero i tassi di fallimento, se fatta bene.

Uno studio del '94 di Hutchins et al. afferma che livelli di copertura superiori al 90% hanno mostrato tassi di rilevamento dei difetti migliori rispetto a insiemi di test più piccoli, e che si sono verificati miglioramenti “significativi” man mano che la copertura aumentava dal 90% al 100%.

Il movimento BDD ha questa pratica divertente di sviluppare specifiche e trasformarle direttamente in unit test. I benefici di unit test chiari e leggibili dagli esseri umani aiutano a documentare il codice e facilitano parte del trasferimento di conoscenza che deve avvenire quando gli sviluppatori inevitabilmente arrivano e se ne vanno, o quando i requisiti dei componenti esistenti cambiano.

L'unit testing, secondo la mia visione, include anche i test generati in stile QuickCheck. L'idea di QuickCheck è che, invece di avere del codice imperativo del tipo if-this-then-that da percorrere, il programmatore può elencare le assunzioni che devono risultare vere per l'output della funzione dati determinati input. QuickCheck quindi genera test che cercano di falsificare queste assunzioni usando l'implementazione, e, se ne trova una, la riduce a un input minimo che le dimostra sbagliate.

È interessante notare che la quantità di scenari che un unit test deve normalmente coprire è fortemente influenzata dal linguaggio di programmazione in cui è scritto. Il che mi porta necessariamente a discutere la sacra guerra santa tra tipizzazione statica e dinamica.

Sistemi di tipi

Hindley-Milner

I sistemi di tipi statici in stile Hindley-Milner, come Haskell o Rust, costringono il programmatore a stabilire contratti che vengono verificati prima che il programma possa essere eseguito.

Ciò che il programmatore scopre, allora, è che sta improvvisamente programmando 2 linguaggi in parallelo: il sistema di tipi fornisce una prova per il programma, mentre esiste anche il programma che soddisfa i requisiti.

Questo consente uno stile di ragionamento sulla correttezza che, combinato con un compilatore utile, permette al programmatore di concentrarsi su ciò che non può essere provato dal sistema di tipi: la logica di business.

Naturalmente, questo non è un vantaggio netto. In molti casi, una soluzione che usa un sistema di tipi dinamico è molto più semplice ed elegante, oppure, in altri casi, i vincoli del sistema di tipi rendono determinate implementazioni praticamente impossibili.

In altri casi, l'uso di Haskell ha permesso di scrivere un programma molto più piccolo in modo significativamente più rapido rispetto alle alternative, tanto che la US Navy ha dovuto ripetere il test per l'incredulità.

L'eleganza è negli occhi di chi guarda, e un'abstrazione tipizzata in modo splendido che si riduce a una semplice macchina a stati durante la compilazione può essere altrettanto attraente quanto una macro LISP rapida e sporca. A volte, tutta quella complessità è difficile da giustificare solo per compiacere il compilatore. Deriva dall'esperienza, dal gusto, e dall'applicazione di un giudizio ponderato nella situazione giusta. Le persone spesso vedono la programmazione come un'arte artigianale. Sì, sappiamo come fare i calcoli, ma con questo trucchetto arriviamo al 90% con il 10% dello sforzo, e potrebbe essere sufficiente. E esploderà proprio in quel caso limite che pensavo non si sarebbe mai verificato. Ma sto divagando.

Debole ma statico

Un po' più accessibili, ma con una verifica meno rigorosa, sono i linguaggi della famiglia OOP in stile C++/Java/C#, così come C e Go.

I sistemi di tipi qui consentono un diverso tipo di flessibilità, e scappatoie più desiderabili verso il mondo dinamico.

Un sistema di tipi più debole, ma comunque statico, fornisce meno garanzie sulla correttezza dei programmi, qualcosa che dobbiamo compensare con il testing e/o con gli standard di codifica. Il Jet Propulsion Lab della NASA, un produttore di massa di rover marziani, mantiene un insieme di linee guida per la programmazione sicura in C. Le loro linee guida sembrano essere efficaci. Opportunity ha superato i suoi originari 90 giorni di attività previsti di 14 anni grazie a una manutenzione attenta. Curiosity sta ancora percorrendo la superficie di Marte, ed viene aggiornato regolarmente.

Dinamico

Parlando del JPL, il folklore di internet conserva la storia dell'uso di LISP al laboratorio della NASA, un linguaggio di programmazione dinamico e funzionale degli anni '60, che viene ancora considerato una delle invenzioni, o addirittura scoperte, più influenti nell'informatica. Il dialetto LISP più comunemente usato oggi è Clojure, che sta guadagnando sempre più popolarità negli ambienti di data science.

I linguaggi dinamici offrono la massima libertà, e poca sicurezza. Più comunemente, l'unico modo per determinare se un pezzo di codice è in qualche modo ragionevole è eseguirlo, il che significa che la nostra strategia di testing deve essere più rigorosa e, effettivamente, approfondita, poiché non c'è un livello successivo su cui fare affidamento.

Probabilmente l'uso più diffuso di un sistema di tipi dinamico oggi proviene da JavaScript. È interessante notare che, mentre le aziende cercano di abbassare la barriera d'ingresso per i contributori scalando lo sviluppo e mantenendo la qualità, Google, Microsoft e Facebook hanno tutte elaborato le proprie soluzioni per introdurre qualche forma di controllo statico dei tipi nel linguaggio.

Anche se Dart di Google non ha visto un'adozione significativa, TypeScript di Microsoft sì, e il suo utilizzo è guidato da progetti grandi e popolari come VSCode. Entrambi gli approcci introducono un linguaggio con controllo statico dei tipi che si compila in JavaScript, rendendo facile introdurlo gradualmente anche nei progetti esistenti.

Al contrario, Flow di Facebook è un analizzatore statico costruito puramente su JavaScript, che introduce il proprio tipo di annotazioni di tipo. L'idea è che, se ci sono annotazioni di tipo in punti strategici, il type checker dovrebbe essere in grado di scoprire se ci sono errori di tipo in una parte del programma tracciando il flusso dei dati.

I programmatori entusiasti vi diranno che entrambi gli approcci alla tipizzazione statica in JavaScript hanno le loro pecche, e avrebbero ragione. Alla fine, molti degli argomenti si riducono a idee soggettive e a gusti personali sull'architettura del software. Sembra difficile negare, tuttavia, che qualche forma di controllo statico dei tipi offra diversi benefici per la scalabilità e il mantenimento dei progetti software.

Le piccole cose che ci sfuggono

La lista delle cose che possiamo fare per garantire la correttezza del software è lontana dall'essere completa. Lo stato dell'arte continua a spingersi oltre, e nuovi approcci guadagnano popolarità rapidamente, specialmente all'interno della comunità della sicurezza.

Nei moduli assolutamente critici, come qualsiasi cosa legata alla crittografia o alla sicurezza, la verifica formale può aumentare la fiducia in parti del sistema, ma è difficile scalarla.

Un sentimento familiare si può ritrovare dietro i principi di LangSec. In molti casi, la potenza e l'espressività dei nostri linguaggi permettono che si insinuino bug involontari. LangSec dice: rendi tutti gli stati non validi non rappresentabili dal linguaggio stesso. Fai in modo che il linguaggio limiti ciò che il programmatore può fare, così può evitare ciò che non dovrebbe fare. Questa è anche la motivazione dietro gli standard di codifica come quelli del JPL, che consentono di ragionare più facilmente sullo stato e sul flusso dei dati in tutto il codice del programma.

Quando siamo ragionevolmente sicuri che ciò di cui abbiamo bisogno sia sufficientemente buono, possiamo iniziare a fuzzarlo. Il fuzzing è fantastico. Consiste tutto nell'alimentare stati inaspettati in un sistema, e aspettare che questi causino fallimenti. Questa semplice idea aiuta a scoprire falle di sicurezza in software popolari o può aiutare a progettare il caos nel cloud.

Come sempre, produrre un sistema stabile e sicuro richiede un'ingegneria rigorosa, nel software tanto quanto nell'architettura. Dobbiamo comprendere i pezzi che compongono il tutto, analizzarne e poi verificarne le interazioni internamente e con l'ambiente. Nonostante i nostri migliori sforzi, i bug si insinueranno sempre, e tutto ciò che possiamo fare è cercare di assicurarci che quelli che rimangono non siano catastrofici.

Tuttavia, una volta che il software va in produzione, la verifica non si ferma. Progettare per l'osservabilità esponendo manopole, tracciamento, avvisi, e raccogliendo un insieme di metriche operative ci aiuta tutti a ragionare sullo stato del sistema mentre è in esecuzione, che è il test definitivo di tutto.

Lo sviluppo software è un processo, ed è praticamente impossibile raggiungere la perfezione. Purché il team abbia un piano per approssimarla, e tutti siano impegnati, possiamo definirlo sufficientemente buono, e poi uscire dall'ufficio per godersi il sole.

Se siete interessati a discutere ancora del giusto e dello sbagliato nel testing del software, scrivetemi una riga – @rhapsodhy oppure via email – mi farebbe piacere conoscere le vostre opinioni!

Qui a Red Sift, quando non stiamo discutendo su qual sia il modo giusto di testare, aiutiamo le organizzazioni orientate alla sicurezza a comunicare con successo con i propri dipendenti, fornitori e clienti, garantendone la fiducia. Visita la nostra homepage per scoprire di più su ciò che facciamo.

Red Sift find out moreRed Sift find out more
Red Sift
Red Sift