Agile, Unit Testing e qualità

Il recente disastro del sito INPS, miseramente crollato nel momento del bisogno, ha riacceso i riflettori sul tema della qualità del software, delle architetture, e dei processi organizzativi,

La qualità è centrale nell’Agile, che enfatizza massimizzazione del valore e ownership del prodotto finale insieme alla sostenibilità dei processi di sviluppo, In questo articolo mi focalizzo sulla sola qualità nel ciclo di vita dello sviluppo software (SDLC) e in particolare sulla sola adozione dei test di unità, mostrando criticità ed esempi pratici del loro utilizzo.

Testing Pyramid e testabilità del codice

Peculiare dell’approccio Agile è il focus sulla scrittura e automazione dei test di unità da parte dei developer che scrivono il codice da testare. Altri strumenti Agile che impattano sulla qualità del software sviluppato sono la definizione di criteri di copertura dei test nelle Definition Of Done, e l’inclusione di test nei criteri di accettazione delle storie. Esiste poi uno specifico approccio Agile al Testing, il Test Driven Development, che prevede la scrittura dei test di unità ancora prima della scrittura del codice da testare. L’Agile mette quindi a disposizione strumenti concreti e processi specifici per assicurare un prodotto finale che massimizza qualità e valore per il cliente finale e sostenibilità dei processi per il team di sviluppo.

Il tema non è però scontato. Capita ancora molto spesso di incontrare senior developer che non hanno mai utilizzato test automatici e faticano nel capire prima e utilizzare poi, un approccio Agile allo sviluppo.

MIke Cohn, uno dei padri dell’Agile, ha inventaro il concetto di Testing Pyramid nel suo libro Succeding with Agile. La piramide ci aiuta a comprendere la quantità di test e l’automazione ottimali per ogni livello: automazione totale per gli unit test, eseguiti a costo “zero” ad ogni deploy, mentre i test end-to-end che verificano il comportamento complessivo del sistema e la sua coerenza con le aspettative dell’utente, sono fatti manualmente e con tempi lunghi,

Scrivere e utilizzare Unit Test non è complesso, ma il codice da testare dev’essere scritto in modo da essere testabile. Cosa significa? Lo spiego con un esempio pratico. Immaginiamo di avere una funzione che, in base all’ora del giorno, decide se salutare con buongiorno, buon pomeriggio, buona sera, etc. Ecco il codice in Python:

"""
Function welcomeWith() returns appropriate greetings
according to the hour of the day
"""
import datetime
def welcomeWith_funct():
    now = datetime.datetime.now()
    hour = now.hour
    if 5 <= hour <= 13:
        welcomeWith = "Good morning!"
    elif 14 <= hour <= 17:
        welcomeWith = "Good afternoon!"
    elif 18 <= hour <= 22:
        welcomeWith = "Good evening!"
    else:
        welcomeWith = "Good night!"
    
    return welcomeWith

Qual è il problema con questa funzione? Che è molto difficile da testare. Infatti il metodo datetime.now() è una sorta di input in continuo cambiamento, per cui ogni chiamata restituisce potenzialmente valori differenti. Per di più il test è condizionato dall’orologio di sistema della macchina che fa girare la funzione, che non è detto sia la stessa macchina su cui girano i test. Diventa così molto difficile se non impossibile testare la logica della funzione, Come dovremmo riscrivere allora il codice per renderlo testabile? Introducendo un nuovo parametro in sostituzione dell'”input nascosto” fornito dalla datetime.now(). Ecco il codice riscritto per poter esser testato facilmente:

def testable_welcomeWith_fuct(hour_of_day):
    hour = hour_of_day
    if 5 <= hour <= 13:
        welcomeWith = "Good morning!"
    elif 14 <= hour <= 17:
        welcomeWith = "Good afternoon!"
    elif 18 <= hour <= 22:
        welcomeWith = "Good evening!"
    else:
        welcomeWith = "Good night!"
    print(welcomeWith, "testable function")
    return welcomeWith

La funzione deve ora essere chiamata con un input esplicito (il parametro hour_of_day) che può essere ripetuto identico ad ogni test e può essere così facilmente testata. Questo è però solo un caso specifico, esistono diversi anti-pattern che rendono il codice difficilmente testabile. Fortuna vuole che questi siano già ampiamente conosciuti (o conoscibili) e documentati. Ad esempio, Google ha reso pubblica nel 2008 la sua guida interna con i criteri per la testabilità del codice, che rappresenta un ottimo punto di partenza per qualsiasi team di sviluppo. Ancora più completo è Clean Code di Robert C. Martin dove sono presentati gli stessi concetti utilizzati in Google come la collezione di principi del SOLID, la Law of Demeter insieme ad altri ancora. Infine anche il TDD, con la scrittura del test prima del codice da testare, guida di fatto verso la scrittura di codice facilmente testabile.

Unit Test. Un esempio concreto

Gli Unit Test si svolgono in tre fasi:

  1. Arrange. In questa fase viene inizializzata la parte di sistema che si vuole testare (System Under Test)
  2. Act. Viene stimolato il System Under Test precedentemente inizializzato
  3. Assert. Si osserva il risultato dello stimolo e lo si confronta con un risultato predeterminato

Il test passa quando il risultato è coerente con quello predeterminato nell’Assert.

Vediamo adesso un esempio pratico di scrittura e utilizzo degli Unit Test. Gli esempi sono scritti in Python utiilzzando pytest, ma tutti i principali linguaggi hanno implementato uno o più framework per il supporto agli Unit Test, a partire dall’originale implementazione del padre dell’eXtreme Programming Kent Beck in Smalltalk nel 1998. Immaginiamo di dover testare un simulatore molto elementare di analisi finanziaria, che ci consenta di calcolare Margini, Tasse, ROI-Return of Investiment di una data organizzazione. Ecco il codice del simulatore.

def margin(r, c=100):
    return r - c
def roi(m, i):
    return m / i
def taxes(m, p):
    return m * p
def net_profit(m, t):
    return m - t

Per il testing useremo pytest, un framework Python evoluto che consente l’uso di fixtures e la scrittura di test molto compatti. Ecco il codice per lo Unit Test, che però contiene due errori. Riesci a vederli? Se non ci riesci per fortuna ci sono i test di unità che te li mostrano 🙂

import roi
#Testing margin func
def test_margin():
    assert roi.margin(100,70) == 30
    assert roi.margin(70) == -30
#Testing  roi func
def test_roi():
    assert round(roi.roi(100,700)) == 0.14
#Testing  taxes  func
def test_taxes():
        assert round(roi.taxes(100, 42)) == 42
#Testing  net profit  func
def test_net_profit():
        assert roi.net_profit(100, 42) == 58

DI seguito l’output di pytest, con il lancio in automatico di tutti i test presenti nella directory. Pytest permette di personalizzare facilmente l’output, il numero e tipo di test lanciati

(venv) PS C:\Users\documents\0-Python-Unit-Test-with-PY> pytest                                               ================================================= test session starts =================================================
platform win32 -- Python 3.8.2, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: C:\Users\documents\0-Python-Unit-Test-with-PY
collected 4 items
test_roi.py .FF.                                                                                                 [100%]
====================================================== FAILURES =======================================================
______________________________________________________ test_roi _______________________________________________________
    def test_roi():
>       assert round(roi.roi(100,700)) == 0.14
E       assert 0 == 0.14
E        +  where 0 = round(0.14285714285714285)
E        +    where 0.14285714285714285 = <function roi at 0x03AFD580>(100, 700)
E        +      where <function roi at 0x03AFD580> = roi.roi
test_roi.py:10: AssertionError
_____________________________________________________ test_taxes ______________________________________________________
    def test_taxes():
>           assert round(roi.taxes(100, 42)) == 42
E           assert 4200 == 42
E            +  where 4200 = round(4200)
E            +    where 4200 = <function taxes at 0x03AFD5C8>(100, 42)
E            +      where <function taxes at 0x03AFD5C8> = roi.taxes
test_roi.py:14: AssertionError
=============================================== short test summary info ===============================================
FAILED test_roi.py::test_roi - assert 0 == 0.14
FAILED test_roi.py::test_taxes - assert 4200 == 42
============================================= 2 failed, 2 passed in 0.13s =============================================
(venv) PS C:\Users\documents\0-Python-Unit-Test-with-PY>     

Il test segnala gli errori esistenti, che siano nella funzione da testare o nel test stesso. In questo caso entrambi gli errori sono proprio nel test:

  1. la funzione round è utilizzata senza indicare il numero di decimali per l’arrotondamento (default=0), e
  2. la funzione taxes() viene testata passando l’aliquota fiscale come valore percentuale (42) invece che decimale (0.42) . Correggiamo e rilanciamo il test per verificare il risultato.
(venv) PS C:\Users\documents\0-Python-Unit-Test-with-PY> pytest test_roi.py -v                     ================================================= test session starts =================================================
platform win32 -- Python 3.8.2, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- c:\users\documents\0-python-unit-test-with-py\venv\scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\0-Python-Unit-Test-with-PY
collected 4 items
test_roi.py::test_margin PASSED                                                                                  [ 25%]
test_roi.py::test_roi PASSED                                                                                     [ 50%]
test_roi.py::test_taxes PASSED                                                                                   [ 75%]
test_roi.py::test_net_profit PASSED                                                                              [100%]
================================================== 4 passed in 0.02s ==================================================
(venv) PS C:\Users\documents\0-Python-Unit-Test-with-PY>    

Ora tutti i quattro test passano senza problemi. Gli Unit Test possono essere lanciati in automatico ad ogni deploy, ad ogni build o in generale ogni volta che si desidera essere certi di non aver introdotto un bug nel sistema. Rappresentano una rete di sicurezza e la garanzia che il sistema si comporti e continui a comportarsi come desiderato.

Che nel 2020 ci siano ancora team di sviluppo che non fanno tesoro degli unit test è un peccato, e un punto debole per le organizzazioni dove questi team svolgono la loro funzione.

Spero che queste righe possano ispirare tutti quei Team che vogliono migliorarsi per entrare finalmente nel nuovo millennio.

Per approfondire

Marcello Del Bono è Product Owner di due team Agile e consulente per programmi di Trasformazione Agile. vanta esperienza pluriennale come ScrumMaster, Agile Coach e Project Manager tradizionale in progetti IT, System Integration, Marketing Digitale, e-commerce nei settori Finance/Banking, Media, Moda, Lifestyle

https://www.linkedin.com/in/marcellodelbono/

Agile, Unit Testing and Quality

The Italian Social Security Office INPS suffered a tremendous debacle few days ago. The INPS website collapsed in time of need after a modest and very predictable traffic peak. Worst, the publicly exposed browser-side software code clearly showed approximation and poor Engineering standards . The fact brought to public attention the issue of Quality in software development and in organizational processes.

Quality is a central theme in Agile, with its enphasis on maximising value and sustainable development process. In this post I am focusing on the specific aspect of quality in Agile Softweare Development Process, and particularly on Unit Test adoption, also showing practical usage examples .

Agile and Testing Pyramid

Agile development left-swithces testing activities, with great emphasis on developer side test automation, test coverages criteria in the Definition of Done as well as in acceptance criteria directly incorporating te automated tests. The rationale for that is about designing better code, and providing a safety net against Regression Problems. The approach in its purest form is exemplified by the TDD-Test Driven Development that creates tests before the coding to be tested. We can say that Agile moves a big part of the testing effort on the developer side promoting automation and unit testing.

That’s not trivial. It’s still quite common to meet senior developers who never used automation test frameworks and with no clue on how to incorporate unit testing in their development cycle.

MIke Cohn explained the Testing Pyramid concept in his book Succeding with Agile. The pyramid helps us to understand the volume and automation of tests in the different layers of the pyramid. Maximum automation and coverage at the unit test level, minimum volume but maximum integration and manual effort at the end-to-end level

Testable and untestable code

Writing unit tests is not difficult, but you have to write your code in a testable way. What does that mean? Let’s say we have a function, which return the appropriate greetings )good morning, good afternoon, etc) based on the hour of the day . Here’s the code in Python


"""
Function welcomeWith() returns appropriate greetings
according to the hour of the day
"""
import datetime
def welcomeWith_funct():
    now = datetime.datetime.now()
    hour = now.hour
    if 5 <= hour <= 13:
        welcomeWith = "Good morning!"
    elif 14 <= hour <= 17:
        welcomeWith = "Good afternoon!"
    elif 18 <= hour <= 22:
        welcomeWith = "Good evening!"
    else:
        welcomeWith = "Good night!"
    
    return welcomeWith

What’s the problem with the above function? The function is very difficult to be tested, The method datetimenow() is an ever changing input , every moment you call it, it returns different results. More, the test returns the hour of the machine that runs the function, there is no guarantee that’s the same machine/hour you are running the test. Man, that function is really hard to test. How can I write the function in a testable way, then?. Here’s the solution: 

def testable_welcomeWith_fuct(hour_of_day):
    hour = hour_of_day
    if 5 <= hour <= 13:
        welcomeWith = "Good morning!"
    elif 14 <= hour <= 17:
        welcomeWith = "Good afternoon!"
    elif 18 <= hour <= 22:
        welcomeWith = "Good evening!"
    else:
        welcomeWith = "Good night!"
    print(welcomeWith, "testable function")
    return welcomeWith

You call now the same function with a parameter ( hour_of_day) so you can have always the same output whit the same input, this way you can test the function!

That’s only a specific case, but there are many anti-patterns causing the code not to be testable. Lucky us, those patterns are already known and documented. Google, for instance, published its guide to testable code, a great starting point for agile teams. Clean Code, by Robert C. Martin is a great book presenting the same concept along with many others. TDD-Test Driven Development, is an agile practice which makes you designing your tests even before starting writing your code. This way, your code becomes naturally testable.

Unit Test, an example

Unit Testing is mad in three phases

  1. ARRANGE. In this phase, you set up the SUS – System Under Stress
  2. CREATE. Applying a stimulus to the SUS
  3. ASSERT. Tracking the results from stimulus->SUS-> and comparing them with pre-asserted results.

Test is passed when predetermined results are coherent with results coming from the stimulus. Test fails when results do not match.

Let’s see a practical example of Unit Test. I have written it in Python, but almost any language in the world has its own Unit Test library, nowadays. Let’s say we want to test a very basic application for financial analysis. The application calculates Margins, Taxes, ROI from a specific organization. Here’s the code:

def margin(r, c=100):
    return r - c
def roi(m, i):
    return m / i
def taxes(m, p):
    return m * p
def net_profit(m, t):
    return m - t

We are using pytest as the library for unit testing, Here’s the code for the tests, it includes two mistakes. Can you spot them? If you can’t lucky us, the unit tests themselves will show us the problems

(venv) PS C:\Users\documents\0-Python-Unit-Test-with-PY> pytest                                               ================================================= test session starts =================================================
platform win32 -- Python 3.8.2, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: C:\Users\documents\0-Python-Unit-Test-with-PY
collected 4 items
test_roi.py .FF.                                                                                                 [100%]
====================================================== FAILURES =======================================================
______________________________________________________ test_roi _______________________________________________________
    def test_roi():
>       assert round(roi.roi(100,700)) == 0.14
E       assert 0 == 0.14
E        +  where 0 = round(0.14285714285714285)
E        +    where 0.14285714285714285 = <function roi at 0x03AFD580>(100, 700)
E        +      where <function roi at 0x03AFD580> = roi.roi
test_roi.py:10: AssertionError
_____________________________________________________ test_taxes ______________________________________________________
    def test_taxes():
>           assert round(roi.taxes(100, 42)) == 42
E           assert 4200 == 42
E            +  where 4200 = round(4200)
E            +    where 4200 = <function taxes at 0x03AFD5C8>(100, 42)
E            +      where <function taxes at 0x03AFD5C8> = roi.taxes
test_roi.py:14: AssertionError
=============================================== short test summary info ===============================================
FAILED test_roi.py::test_roi - assert 0 == 0.14
FAILED test_roi.py::test_taxes - assert 4200 == 42
============================================= 2 failed, 2 passed in 0.13s =============================================
(venv) PS C:\Users\documents\0-Python-Unit-Test-with-PY>  

The test is showing us two trivial errors:

  1. Round() function used is missing the correct number of decimals to be used (default=0), and
  2. Taxes() needs as input a percentage value (42) not the decimal absolute value (0.42) . Let’s solve the issue, and see the result.
(venv) PS C:\Users\documents\0-Python-Unit-Test-with-PY> pytest test_roi.py -v                     ================================================= test session starts =================================================
platform win32 -- Python 3.8.2, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- c:\users\documents\0-python-unit-test-with-py\venv\scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\0-Python-Unit-Test-with-PY
collected 4 items
test_roi.py::test_margin PASSED                                                                                  [ 25%]
test_roi.py::test_roi PASSED                                                                                     [ 50%]
test_roi.py::test_taxes PASSED                                                                                   [ 75%]
test_roi.py::test_net_profit PASSED                                                                              [100%]
================================================== 4 passed in 0.02s ==================================================
(venv) PS C:\Users\documents\0-Python-Unit-Test-with-PY>    

Now all the tests are passed 🙂 Unit Test can be started at any build, any deploy, any pipeline activation. They are safety networks against regression problems and silent guardians of the quality in your system.

THat’s too bad that in 2020 many teams are still NOT USING UNIT TESTS, thus causing weaknesses and problems in their organizations.

I hope these post will inspire you and your team in starting using and mastering Unit Tests 🙂


More info

Marcello Del Bono è Product Owner di due team Agile e consulente per programmi di Trasformazione Agile. vanta esperienza pluriennale come ScrumMaster, Agile Coach e Project Manager tradizionale in progetti IT, System Integration, Marketing Digitale, e-commerce nei settori Finance/Banking, Media, Moda, Lifestyle

https://www.linkedin.com/in/marcellodelbono/