Lecture Notes 7#

Testing#

Testing frameworks are based on the assert statement. If the expressions following assert is

  • True: go on as usual

  • False: an AssertionError makes a program stop or can be handled by a framework (some error report)

assert True
assert False
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[2], line 1
----> 1 assert False

AssertionError: 

Testing with doctest#

Example: the fat mix#

Here we refactor the original code

import sys

def main():

    if len(sys.argv) > 1:
        required_volume = float(sys.argv[1])    
    else:
        print("Usage:", sys.argv[0], "")
        exit()

    required_fat_content = 3 # in percent
    available_fat_content = 1.5 
    cream_fat_content = 40

    conversion_ratio = (required_fat_content - available_fat_content)/(cream_fat_content-available_fat_content)
    replaced_volume =  conversion_ratio * required_volume

    print(f'Replaced volume: {replaced_volume:5.1g}')


main()

by extracting the conversion_ratio calculation to a function and introduce a test in the docstring

import sys

def get_conversion_ratio(required_fat_content, available_fat_content, cream_fat_content): """ Calculate fat conversion ration >>> get_conversion_ratio(3, 3, 40) 0.0 >>> round(get_conversion_ratio(3, 1.5, 40), 2) 0.04 """ conversion_ratio = (required_fat_content - available_fat_content)/(cream_fat_content-available_fat_content) return conversion_ratio
def main(): if len(sys.argv) > 1: required_volume = float(sys.argv[1]) else: print("Usage:", sys.argv[0], "") exit() required_fat_content = 3 # in percent available_fat_content = 1.5 cream_fat_content = 40
conversion_ratio = get_conversion_ratio(required_fat_content, available_fat_content, cream_fat_content)
replaced_volume = conversion_ratio * required_volume print(f'Replaced volume: {replaced_volume:5.1g}') print(f"__name__ = {__name__}") if __name__ == "__main__": main()
  • The docstring contains a copy-paste of an interactive session in the REPL

  • The doctest library recognizes these code snippets as tests and are executed with

!python -m doctest /home/python-lll/projects/recipe/green2red.py -v
__name__ = green2red
Trying:
    get_conversion_ratio(3, 3, 40)
Expecting:
    0.0
ok
Trying:
    round(get_conversion_ratio(3, 1.5, 40), 2)
Expecting:
    0.04
ok
2 items had no tests:
    green2red
    green2red.main
1 items passed all tests:
   2 tests in green2red.get_conversion_ratio
2 tests in 3 items.
2 passed and 0 failed.
Test passed.

What does __name__ == "__main__" mean?#

Observe how the special variable __name__ is used

The line

print(f"__name__ = {__name__}")

shows different output depending on context:

  • during import (what testing libraries do)

    __name__ = green2red

  • during normal run

    __name__ = __main__

We only want to execute the full code during a normal run, therefore we have the if clause in the end

if __name__ == "__main__":
    main()

Testing with pytest#

An equivalent test for the pytest framework means to define separate test functions for these cases in a separate file. They normally contain one or sometimes more assert statements to compare calculated vs expected values

import green2red

def test_conversion_ratio_trivial():
    calculated = green2red.get_conversion_ratio(3, 3, 40)
    expected = 0
    assert calculated == expected

def test_conversion_ratio():
    calculated = green2red.get_conversion_ratio(3, 1.5, 40)
    expected = 0.039
    assert abs(calculated - expected) < .001
!python -m pytest /home/python-lll/projects/recipe/test_green2red.py -v
============================= test session starts ==============================
platform linux -- Python 3.11.0, pytest-7.1.2, pluggy-1.0.0 -- /home/python-lll/miniconda3/envs/bb1000/bin/python
cachedir: .pytest_cache
rootdir: /home/python-lll
plugins: anyio-3.5.0
collecting ... 
collected 2 items                                                              

../../projects/recipe/test_green2red.py::test_conversion_ratio_trivial PASSED [ 50%]
../../projects/recipe/test_green2red.py::test_conversion_ratio PASSED    [100%]

============================== 2 passed in 0.00s ===============================