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/